1 from PyQt4.QtCore import *
2 from PyQt4.QtGui import *
3 from decimal import Decimal as D
4 from util import get_resource_path as rsrc
15 import lib.gui_qt as gui_qt
17 import electrum.gui_qt as gui_qt
19 bitcoin = lambda v: v * 100000000
21 def IconButton(filename, parent=None):
22 pixmap = QPixmap(filename)
24 return QPushButton(icon, "", parent)
29 self.emit(SIGNAL('timersignal'))
34 def __init__(self, wallet):
36 self.app = QApplication(sys.argv)
37 QDir.setCurrent(rsrc())
38 with open(rsrc("style.css")) as style_file:
39 self.app.setStyleSheet(style_file.read())
42 actuator = MiniActuator(self.wallet)
43 self.mini = MiniWindow(actuator, self.expand)
44 driver = MiniDriver(self.wallet, self.mini)
51 self.expert = gui_qt.ElectrumWindow(self.wallet)
52 self.expert.app = self.app
53 self.expert.connect_slots(timer)
54 self.expert.update_wallet()
56 sys.exit(self.app.exec_())
62 def set_url(self, url):
63 payto, amount, label, message, signature, identity, url = \
64 self.wallet.parse_url(url, self.show_message, self.show_question)
65 self.mini.set_payment_fields(payto, amount)
67 def show_message(self, message):
68 QMessageBox.information(self.mini, _("Message"), message, _("OK"))
70 def show_question(self, message):
71 choice = QMessageBox.question(self.mini, _("Message"), message,
72 QMessageBox.Yes|QMessageBox.No,
74 return choice == QMessageBox.Yes
76 def restore_or_create(self):
77 qt_gui_object = gui_qt.ElectrumGui(self.wallet, self.app)
78 return qt_gui_object.restore_or_create()
80 class MiniWindow(QDialog):
82 def __init__(self, actuator, expand_callback):
83 super(MiniWindow, self).__init__()
85 self.actuator = actuator
87 accounts_button = IconButton(rsrc("icons", "accounts.png"))
88 accounts_button.setObjectName("accounts_button")
90 self.accounts_selector = QMenu()
91 accounts_button.setMenu(self.accounts_selector)
93 interact_button = IconButton(rsrc("icons", "interact.png"))
94 interact_button.setObjectName("interact_button")
96 app_menu = QMenu(interact_button)
97 report_action = app_menu.addAction(_("&Report Bug"))
98 about_action = app_menu.addAction(_("&About Electrum"))
99 app_menu.addSeparator()
100 quit_action = app_menu.addAction(_("&Quit"))
101 interact_button.setMenu(app_menu)
103 self.connect(report_action, SIGNAL("triggered()"),
104 self.show_report_bug)
105 self.connect(about_action, SIGNAL("triggered()"), self.show_about)
106 self.connect(quit_action, SIGNAL("triggered()"), self.close)
108 expand_button = IconButton(rsrc("icons", "expand.png"))
109 expand_button.setObjectName("expand_button")
110 self.connect(expand_button, SIGNAL("clicked()"), expand_callback)
112 self.btc_balance = None
113 self.quote_currencies = ("EUR", "USD", "GBP")
114 self.exchanger = exchange_rate.Exchanger(self)
115 # Needed because price discovery is done in a different thread
116 # which needs to be sent back to this main one to update the GUI
117 self.connect(self, SIGNAL("refresh_balance()"), self.refresh_balance)
119 self.balance_label = BalanceLabel(self.change_quote_currency)
120 self.balance_label.setObjectName("balance_label")
122 copy_button = QPushButton(_("&Copy My Address"))
123 copy_button.setObjectName("copy_button")
124 copy_button.setDefault(True)
125 self.connect(copy_button, SIGNAL("clicked()"),
126 self.actuator.copy_address)
128 self.address_input = TextedLineEdit(_("Enter a Bitcoin address..."))
129 self.address_input.setObjectName("address_input")
130 self.connect(self.address_input, SIGNAL("textEdited(QString)"),
131 self.address_field_changed)
132 metrics = QFontMetrics(qApp.font())
133 self.address_input.setMinimumWidth(
134 metrics.width("1E4vM9q25xsyDwWwdqHUWnwshdWC9PykmL"))
136 self.address_completions = QStringListModel()
137 address_completer = QCompleter(self.address_input)
138 address_completer.setCaseSensitivity(False)
139 address_completer.setModel(self.address_completions)
140 self.address_input.setCompleter(address_completer)
142 self.valid_address = QCheckBox()
143 self.valid_address.setObjectName("valid_address")
144 self.valid_address.setEnabled(False)
145 self.valid_address.setChecked(False)
147 address_layout = QHBoxLayout()
148 address_layout.addWidget(self.address_input)
149 address_layout.addWidget(self.valid_address)
151 self.amount_input = TextedLineEdit(_("... and amount"))
152 self.amount_input.setObjectName("amount_input")
153 # This is changed according to the user's displayed balance
154 self.amount_validator = QDoubleValidator(self.amount_input)
155 self.amount_validator.setNotation(QDoubleValidator.StandardNotation)
156 self.amount_validator.setDecimals(8)
157 self.amount_input.setValidator(self.amount_validator)
159 self.connect(self.amount_input, SIGNAL("textChanged(QString)"),
160 self.amount_input_changed)
162 amount_layout = QHBoxLayout()
163 amount_layout.addWidget(self.amount_input)
164 amount_layout.addStretch()
166 send_button = QPushButton(_("&Send"))
167 send_button.setObjectName("send_button")
168 self.connect(send_button, SIGNAL("clicked()"), self.send)
170 main_layout = QGridLayout(self)
171 main_layout.addWidget(accounts_button, 0, 0)
172 main_layout.addWidget(interact_button, 1, 0)
173 main_layout.addWidget(expand_button, 2, 0)
175 main_layout.addWidget(self.balance_label, 0, 1)
176 main_layout.addWidget(copy_button, 0, 2)
178 main_layout.addLayout(address_layout, 1, 1, 1, -1)
180 main_layout.addLayout(amount_layout, 2, 1)
181 main_layout.addWidget(send_button, 2, 2)
183 self.setWindowTitle("Electrum")
184 self.setWindowFlags(Qt.Window|Qt.MSWindowsFixedSizeDialogHint)
185 self.layout().setSizeConstraint(QLayout.SetFixedSize)
186 self.setObjectName("main_window")
189 def closeEvent(self, event):
190 super(MiniWindow, self).closeEvent(event)
193 def set_payment_fields(self, dest_address, amount):
194 self.address_input.become_active()
195 self.address_input.setText(dest_address)
196 self.address_field_changed(dest_address)
197 self.amount_input.become_active()
198 self.amount_input.setText(amount)
203 def deactivate(self):
206 def change_quote_currency(self):
207 self.quote_currencies = \
208 self.quote_currencies[1:] + self.quote_currencies[0:1]
209 self.refresh_balance()
211 def refresh_balance(self):
212 if self.btc_balance is None:
213 # Price has been discovered before wallet has been loaded
214 # and server connect... so bail.
216 self.set_balances(self.btc_balance)
217 self.amount_input_changed(self.amount_input.text())
219 def set_balances(self, btc_balance):
220 self.btc_balance = btc_balance
221 quote_text = self.create_quote_text(btc_balance)
223 quote_text = "(%s)" % quote_text
224 btc_balance = "%.2f" % (btc_balance / bitcoin(1))
225 self.balance_label.set_balance_text(btc_balance, quote_text)
226 main_account_info = \
227 "Checking - %s BTC %s" % (btc_balance, quote_text)
228 self.setWindowTitle("Electrum - %s" % main_account_info)
229 self.accounts_selector.clear()
230 self.accounts_selector.addAction("%s" % main_account_info)
232 def amount_input_changed(self, amount_text):
234 amount = D(str(amount_text))
235 except decimal.InvalidOperation:
236 self.balance_label.show_balance()
238 quote_text = self.create_quote_text(amount * bitcoin(1))
240 self.balance_label.set_amount_text(quote_text)
241 self.balance_label.show_amount()
243 self.balance_label.show_balance()
245 def create_quote_text(self, btc_balance):
246 quote_currency = self.quote_currencies[0]
247 quote_balance = self.exchanger.exchange(btc_balance, quote_currency)
248 if quote_balance is None:
251 quote_text = "%.2f %s" % ((quote_balance / bitcoin(1)),
256 if self.actuator.send(self.address_input.text(),
257 self.amount_input.text(), self):
258 self.address_input.become_inactive()
259 self.amount_input.become_inactive()
261 def address_field_changed(self, address):
262 if self.actuator.is_valid(address):
263 self.valid_address.setChecked(True)
265 self.valid_address.setChecked(False)
267 def update_completions(self, completions):
268 self.address_completions.setStringList(completions)
270 def show_about(self):
271 QMessageBox.about(self, "Electrum",
272 _("Electrum's focus is speed, with low resource usage and simplifying Bitcoin. You do not need to perform regular backups, because your wallet can be recovered from a secret phrase that you can memorize or write on paper. Startup times are instant because it operates in conjuction with high-performance servers that handle the most complicated parts of the Bitcoin system."))
274 def show_report_bug(self):
275 QMessageBox.information(self, "Electrum - " + _("Reporting Bugs"),
276 _("Email bug reports to %s") % "genjix" + "@" + "riseup.net")
278 class BalanceLabel(QLabel):
284 def __init__(self, change_quote_currency, parent=None):
285 super(QLabel, self).__init__(_("Connecting..."), parent)
286 self.change_quote_currency = change_quote_currency
287 self.state = self.SHOW_CONNECTING
288 self.balance_text = ""
289 self.amount_text = ""
291 def mousePressEvent(self, event):
292 if self.state != self.SHOW_CONNECTING:
293 self.change_quote_currency()
295 def set_balance_text(self, btc_balance, quote_text):
296 if self.state == self.SHOW_CONNECTING:
297 self.state = self.SHOW_BALANCE
298 self.balance_text = "<span style='font-size: 16pt'>%s</span> <span style='font-size: 10pt'>BTC</span> <span style='font-size: 10pt'>%s</span>" % (btc_balance, quote_text)
299 if self.state == self.SHOW_BALANCE:
300 self.setText(self.balance_text)
302 def set_amount_text(self, quote_text):
303 self.amount_text = "<span style='font-size: 10pt'>%s</span>" % quote_text
304 if self.state == self.SHOW_AMOUNT:
305 self.setText(self.amount_text)
307 def show_balance(self):
308 if self.state == self.SHOW_AMOUNT:
309 self.state = self.SHOW_BALANCE
310 self.setText(self.balance_text)
312 def show_amount(self):
313 if self.state == self.SHOW_BALANCE:
314 self.state = self.SHOW_AMOUNT
315 self.setText(self.amount_text)
317 class TextedLineEdit(QLineEdit):
319 def __init__(self, inactive_text, parent=None):
320 super(QLineEdit, self).__init__(parent)
321 self.inactive_text = inactive_text
322 self.become_inactive()
324 def mousePressEvent(self, event):
325 if self.isReadOnly():
327 QLineEdit.mousePressEvent(self, event)
329 def focusOutEvent(self, event):
330 if self.text() == "":
331 self.become_inactive()
332 QLineEdit.focusOutEvent(self, event)
334 def focusInEvent(self, event):
335 if self.isReadOnly():
337 QLineEdit.focusInEvent(self, event)
339 def become_inactive(self):
340 self.setText(self.inactive_text)
341 self.setReadOnly(True)
342 self.recompute_style()
344 def become_active(self):
346 self.setReadOnly(False)
347 self.recompute_style()
349 def recompute_style(self):
350 qApp.style().unpolish(self)
351 qApp.style().polish(self)
352 # also possible but more expensive:
353 #qApp.setStyleSheet(qApp.styleSheet())
355 def ok_cancel_buttons(dialog):
356 row_layout = QHBoxLayout()
357 row_layout.addStretch(1)
358 ok_button = QPushButton(_("OK"))
359 row_layout.addWidget(ok_button)
360 ok_button.clicked.connect(dialog.accept)
361 cancel_button = QPushButton(_("Cancel"))
362 row_layout.addWidget(cancel_button)
363 cancel_button.clicked.connect(dialog.reject)
366 class PasswordDialog(QDialog):
368 def __init__(self, parent):
369 super(QDialog, self).__init__(parent)
373 self.password_input = QLineEdit()
374 self.password_input.setEchoMode(QLineEdit.Password)
376 main_layout = QVBoxLayout(self)
377 message = _('Please enter your password')
378 main_layout.addWidget(QLabel(message))
382 grid.addWidget(QLabel(_('Password')), 1, 0)
383 grid.addWidget(self.password_input, 1, 1)
384 main_layout.addLayout(grid)
386 main_layout.addLayout(ok_cancel_buttons(self))
387 self.setLayout(main_layout)
392 return unicode(self.password_input.text())
396 def __init__(self, wallet):
399 def copy_address(self):
400 addrs = [addr for addr in self.wallet.all_addresses()
401 if not self.wallet.is_change(addr)]
402 qApp.clipboard().setText(random.choice(addrs))
404 def send(self, address, amount, parent_window):
405 dest_address = self.fetch_destination(address)
407 if dest_address is None or not self.wallet.is_valid(dest_address):
408 QMessageBox.warning(parent_window, _('Error'),
409 _('Invalid Bitcoin Address') + ':\n' + address, _('OK'))
412 convert_amount = lambda amount: \
413 int(D(unicode(amount)) * bitcoin(1))
414 amount = convert_amount(amount)
416 if self.wallet.use_encryption:
417 password_dialog = PasswordDialog(parent_window)
418 password = password_dialog.run()
426 if amount < bitcoin(1) / 10:
428 fee = bitcoin(1) / 100
431 tx = self.wallet.mktx(dest_address, amount, "", password, fee)
432 except BaseException as error:
433 QMessageBox.warning(parent_window, _('Error'), str(error), _('OK'))
436 status, message = self.wallet.sendtx(tx)
438 QMessageBox.warning(parent_window, _('Error'), message, _('OK'))
441 QMessageBox.information(parent_window, '',
442 _('Payment sent.') + '\n' + message, _('OK'))
445 def fetch_destination(self, address):
446 recipient = unicode(address).strip()
449 match1 = re.match("^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$",
452 # label or alias, with address in brackets
453 match2 = re.match("(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>",
458 self.wallet.get_alias(recipient, True,
459 self.show_message, self.question)
462 return match2.group(2)
466 def is_valid(self, address):
467 return self.wallet.is_valid(address)
469 class MiniDriver(QObject):
476 def __init__(self, wallet, window):
477 super(QObject, self).__init__()
482 self.wallet.register_callback(self.update_callback)
487 self.connect(self, SIGNAL("updatesignal()"), self.update)
489 # This is a hack to workaround that Qt does not like changing the
490 # window properties from this other thread before the runloop has
492 def update_callback(self):
493 self.emit(SIGNAL("updatesignal()"))
496 if not self.wallet.interface:
498 elif not self.wallet.interface.is_connected:
500 elif not self.wallet.blocks == -1:
502 elif not self.wallet.is_up_to_date:
507 if self.wallet.up_to_date:
508 self.update_balance()
509 self.update_completions()
511 def initializing(self):
512 if self.state == self.INITIALIZING:
514 self.state = self.INITIALIZING
515 self.window.deactivate()
517 def connecting(self):
518 if self.state == self.CONNECTING:
520 self.state = self.CONNECTING
521 self.window.deactivate()
523 def synchronizing(self):
524 if self.state == self.SYNCHRONIZING:
526 self.state = self.SYNCHRONIZING
527 self.window.deactivate()
530 if self.state == self.READY:
532 self.state = self.READY
533 self.window.activate()
535 def update_balance(self):
536 conf_balance, unconf_balance = self.wallet.get_balance()
537 balance = D(conf_balance + unconf_balance)
538 self.window.set_balances(balance)
540 def update_completions(self):
542 for addr, label in self.wallet.labels.items():
543 if addr in self.wallet.addressbook:
544 completions.append("%s <%s>" % (label, addr))
545 completions = completions + self.wallet.aliases.keys()
546 self.window.update_completions(completions)
548 if __name__ == "__main__":
549 app = QApplication(sys.argv)
550 with open(rsrc("style.css")) as style_file:
551 app.setStyleSheet(style_file.read())
553 sys.exit(app.exec_())