1 from PyQt4.QtCore import *
2 from PyQt4.QtGui import *
3 from decimal import Decimal as D
14 import lib.gui_qt as gui_qt
16 import electrum.gui_qt as gui_qt
18 bitcoin = lambda v: v * 100000000
20 def IconButton(filename, parent=None):
21 pixmap = QPixmap(filename)
23 return QPushButton(icon, "", parent)
28 self.emit(SIGNAL('timersignal'))
33 def __init__(self, wallet):
35 self.app = QApplication(sys.argv)
36 with open("data/style.css") as style_file:
37 self.app.setStyleSheet(style_file.read())
40 actuator = MiniActuator(self.wallet)
41 self.mini = MiniWindow(actuator, self.expand)
42 driver = MiniDriver(self.wallet, self.mini)
49 self.expert = gui_qt.ElectrumWindow(self.wallet)
50 self.expert.app = self.app
51 self.expert.connect_slots(timer)
52 self.expert.update_wallet()
54 sys.exit(self.app.exec_())
60 def set_url(self, url):
61 payto, amount, label, message, signature, identity, url = \
62 self.wallet.parse_url(url, self.show_message, self.show_question)
63 self.mini.set_payment_fields(payto, amount)
65 def show_message(self, message):
66 QMessageBox.information(self.mini, _("Message"), message, _("OK"))
68 def show_question(self, message):
69 choice = QMessageBox.question(self.mini, _("Message"), message,
70 QMessageBox.Yes|QMessageBox.No,
72 return choice == QMessageBox.Yes
74 class MiniWindow(QDialog):
76 def __init__(self, actuator, expand_callback):
77 super(MiniWindow, self).__init__()
79 self.actuator = actuator
81 accounts_button = IconButton("data/icons/accounts.png")
82 accounts_button.setObjectName("accounts_button")
84 self.accounts_selector = QMenu()
85 accounts_button.setMenu(self.accounts_selector)
87 interact_button = IconButton("data/icons/interact.png")
88 interact_button.setObjectName("interact_button")
90 app_menu = QMenu(interact_button)
91 report_action = app_menu.addAction(_("&Report Bug"))
92 about_action = app_menu.addAction(_("&About Electrum"))
93 app_menu.addSeparator()
94 quit_action = app_menu.addAction(_("&Quit"))
95 interact_button.setMenu(app_menu)
97 self.connect(report_action, SIGNAL("triggered()"),
99 self.connect(about_action, SIGNAL("triggered()"), self.show_about)
100 self.connect(quit_action, SIGNAL("triggered()"), self.close)
102 expand_button = IconButton("data/icons/expand.png")
103 expand_button.setObjectName("expand_button")
104 self.connect(expand_button, SIGNAL("clicked()"), expand_callback)
106 self.btc_balance = None
107 self.quote_currencies = ("EUR", "USD", "GBP")
108 self.exchanger = exchange_rate.Exchanger(self)
109 # Needed because price discovery is done in a different thread
110 # which needs to be sent back to this main one to update the GUI
111 self.connect(self, SIGNAL("refresh_balance()"), self.refresh_balance)
113 self.balance_label = BalanceLabel(self.change_quote_currency)
114 self.balance_label.setObjectName("balance_label")
116 copy_button = QPushButton(_("&Copy Address"))
117 copy_button.setObjectName("copy_button")
118 copy_button.setDefault(True)
119 self.connect(copy_button, SIGNAL("clicked()"),
120 self.actuator.copy_address)
122 self.address_input = TextedLineEdit(_("Enter a Bitcoin address..."))
123 self.address_input.setObjectName("address_input")
124 self.connect(self.address_input, SIGNAL("textEdited(QString)"),
125 self.address_field_changed)
126 metrics = QFontMetrics(qApp.font())
127 self.address_input.setMinimumWidth(
128 metrics.width("1E4vM9q25xsyDwWwdqHUWnwshdWC9PykmL"))
130 self.address_completions = QStringListModel()
131 address_completer = QCompleter(self.address_input)
132 address_completer.setCaseSensitivity(False)
133 address_completer.setModel(self.address_completions)
134 self.address_input.setCompleter(address_completer)
135 self.address_completions.setStringList(["1brmlab", "hello"])
137 self.valid_address = QCheckBox()
138 self.valid_address.setObjectName("valid_address")
139 self.valid_address.setEnabled(False)
140 self.valid_address.setChecked(False)
142 address_layout = QHBoxLayout()
143 address_layout.addWidget(self.address_input)
144 address_layout.addWidget(self.valid_address)
146 self.amount_input = TextedLineEdit(_("... and amount"))
147 self.amount_input.setObjectName("amount_input")
148 # This is changed according to the user's displayed balance
149 self.amount_validator = QDoubleValidator(self.amount_input)
150 self.amount_validator.setNotation(QDoubleValidator.StandardNotation)
151 self.amount_validator.setDecimals(8)
152 self.amount_input.setValidator(self.amount_validator)
154 self.connect(self.amount_input, SIGNAL("textChanged(QString)"),
155 self.amount_input_changed)
157 amount_layout = QHBoxLayout()
158 amount_layout.addWidget(self.amount_input)
159 amount_layout.addStretch()
161 send_button = QPushButton(_("&Send"))
162 send_button.setObjectName("send_button")
163 self.connect(send_button, SIGNAL("clicked()"), self.send)
165 main_layout = QGridLayout(self)
166 main_layout.addWidget(accounts_button, 0, 0)
167 main_layout.addWidget(interact_button, 1, 0)
168 main_layout.addWidget(expand_button, 2, 0)
170 main_layout.addWidget(self.balance_label, 0, 1)
171 main_layout.addWidget(copy_button, 0, 2)
173 main_layout.addLayout(address_layout, 1, 1, 1, -1)
175 main_layout.addLayout(amount_layout, 2, 1)
176 main_layout.addWidget(send_button, 2, 2)
178 self.setWindowTitle("Electrum")
179 self.setWindowFlags(Qt.Window|Qt.MSWindowsFixedSizeDialogHint)
180 self.layout().setSizeConstraint(QLayout.SetFixedSize)
181 self.setObjectName("main_window")
184 def closeEvent(self, event):
185 super(MiniWindow, self).closeEvent(event)
188 def set_payment_fields(self, dest_address, amount):
189 self.address_input.become_active()
190 self.address_input.setText(dest_address)
191 self.address_field_changed(dest_address)
192 self.amount_input.become_active()
193 self.amount_input.setText(amount)
198 def deactivate(self):
201 def change_quote_currency(self):
202 self.quote_currencies = \
203 self.quote_currencies[1:] + self.quote_currencies[0:1]
204 self.refresh_balance()
206 def refresh_balance(self):
207 if self.btc_balance is None:
208 # Price has been discovered before wallet has been loaded
209 # and server connect... so bail.
211 self.set_balances(self.btc_balance)
212 self.amount_input_changed(self.amount_input.text())
214 def set_balances(self, btc_balance):
215 self.btc_balance = btc_balance
216 quote_text = self.create_quote_text(btc_balance)
218 quote_text = "(%s)" % quote_text
219 btc_balance = "%.2f" % (btc_balance / bitcoin(1))
220 self.balance_label.set_balance_text(btc_balance, quote_text)
221 main_account_info = \
222 "Checking - %s BTC %s" % (btc_balance, quote_text)
223 self.setWindowTitle("Electrum - %s" % main_account_info)
224 self.accounts_selector.clear()
225 self.accounts_selector.addAction("%s" % main_account_info)
227 def amount_input_changed(self, amount_text):
229 amount = D(str(amount_text))
230 except decimal.InvalidOperation:
231 self.balance_label.show_balance()
233 quote_text = self.create_quote_text(amount * bitcoin(1))
235 self.balance_label.set_amount_text(quote_text)
236 self.balance_label.show_amount()
238 self.balance_label.show_balance()
240 def create_quote_text(self, btc_balance):
241 quote_currency = self.quote_currencies[0]
242 quote_balance = self.exchanger.exchange(btc_balance, quote_currency)
243 if quote_balance is None:
246 quote_text = "%.2f %s" % ((quote_balance / bitcoin(1)),
251 if self.actuator.send(self.address_input.text(),
252 self.amount_input.text(), self):
253 self.address_input.become_inactive()
254 self.amount_input.become_inactive()
256 def address_field_changed(self, address):
257 if self.actuator.is_valid(address):
258 self.valid_address.setChecked(True)
260 self.valid_address.setChecked(False)
262 def update_completions(self, completions):
263 self.address_completions.setStringList(completions)
265 def show_about(self):
266 QMessageBox.about(self, "Electrum",
267 _("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."))
269 def show_report_bug(self):
270 QMessageBox.information(self, "Electrum - " + _("Reporting Bugs"),
271 _("Email bug reports to %s") % "genjix" + "@" + "riseup.net")
273 class BalanceLabel(QLabel):
279 def __init__(self, change_quote_currency, parent=None):
280 super(QLabel, self).__init__(_("Connecting..."), parent)
281 self.change_quote_currency = change_quote_currency
282 self.state = self.SHOW_CONNECTING
283 self.balance_text = ""
284 self.amount_text = ""
286 def mousePressEvent(self, event):
287 if self.state != self.SHOW_CONNECTING:
288 self.change_quote_currency()
290 def set_balance_text(self, btc_balance, quote_text):
291 if self.state == self.SHOW_CONNECTING:
292 self.state = self.SHOW_BALANCE
293 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)
294 if self.state == self.SHOW_BALANCE:
295 self.setText(self.balance_text)
297 def set_amount_text(self, quote_text):
298 self.amount_text = "<span style='font-size: 10pt'>%s</span>" % quote_text
299 if self.state == self.SHOW_AMOUNT:
300 self.setText(self.amount_text)
302 def show_balance(self):
303 if self.state == self.SHOW_AMOUNT:
304 self.state = self.SHOW_BALANCE
305 self.setText(self.balance_text)
307 def show_amount(self):
308 if self.state == self.SHOW_BALANCE:
309 self.state = self.SHOW_AMOUNT
310 self.setText(self.amount_text)
312 class TextedLineEdit(QLineEdit):
314 def __init__(self, inactive_text, parent=None):
315 super(QLineEdit, self).__init__(parent)
316 self.inactive_text = inactive_text
317 self.become_inactive()
319 def mousePressEvent(self, event):
320 if self.isReadOnly():
322 QLineEdit.mousePressEvent(self, event)
324 def focusOutEvent(self, event):
325 if self.text() == "":
326 self.become_inactive()
327 QLineEdit.focusOutEvent(self, event)
329 def focusInEvent(self, event):
330 if self.isReadOnly():
332 QLineEdit.focusInEvent(self, event)
334 def become_inactive(self):
335 self.setText(self.inactive_text)
336 self.setReadOnly(True)
337 self.recompute_style()
339 def become_active(self):
341 self.setReadOnly(False)
342 self.recompute_style()
344 def recompute_style(self):
345 qApp.style().unpolish(self)
346 qApp.style().polish(self)
347 # also possible but more expensive:
348 #qApp.setStyleSheet(qApp.styleSheet())
350 def ok_cancel_buttons(dialog):
351 row_layout = QHBoxLayout()
352 row_layout.addStretch(1)
353 ok_button = QPushButton("OK")
354 row_layout.addWidget(ok_button)
355 ok_button.clicked.connect(dialog.accept)
356 cancel_button = QPushButton("Cancel")
357 row_layout.addWidget(cancel_button)
358 cancel_button.clicked.connect(dialog.reject)
361 class PasswordDialog(QDialog):
363 def __init__(self, parent):
364 super(QDialog, self).__init__(parent)
368 self.password_input = QLineEdit()
369 self.password_input.setEchoMode(QLineEdit.Password)
371 main_layout = QVBoxLayout(self)
372 message = _('Please enter your password')
373 main_layout.addWidget(QLabel(message))
377 grid.addWidget(QLabel(_('Password')), 1, 0)
378 grid.addWidget(self.password_input, 1, 1)
379 main_layout.addLayout(grid)
381 main_layout.addLayout(ok_cancel_buttons(self))
382 self.setLayout(main_layout)
387 return unicode(self.password_input.text())
391 def __init__(self, wallet):
394 def copy_address(self):
395 addrs = [addr for addr in self.wallet.all_addresses()
396 if not self.wallet.is_change(addr)]
397 qApp.clipboard().setText(random.choice(addrs))
399 def send(self, address, amount, parent_window):
400 dest_address = self.fetch_destination(address)
402 if dest_address is None or not self.wallet.is_valid(dest_address):
403 QMessageBox.warning(parent_window, _('Error'),
404 _('Invalid Bitcoin Address') + ':\n' + address, _('OK'))
407 convert_amount = lambda amount: \
408 int(D(unicode(amount)) * bitcoin(1))
409 amount = convert_amount(amount)
411 if self.wallet.use_encryption:
412 password_dialog = PasswordDialog(parent_window)
413 password = password_dialog.run()
421 if amount < bitcoin(1) / 10:
423 fee = bitcoin(1) / 100
426 tx = self.wallet.mktx(dest_address, amount, "", password, fee)
427 except BaseException as error:
428 QMessageBox.warning(parent_window, _('Error'), str(error), _('OK'))
431 status, message = self.wallet.sendtx(tx)
433 QMessageBox.warning(parent_window, _('Error'), message, _('OK'))
436 QMessageBox.information(parent_window, '',
437 _('Payment sent.') + '\n' + message, _('OK'))
440 def fetch_destination(self, address):
441 recipient = unicode(address).strip()
444 match1 = re.match("^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$",
447 # label or alias, with address in brackets
448 match2 = re.match("(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>",
453 self.wallet.get_alias(recipient, True,
454 self.show_message, self.question)
457 return match2.group(2)
461 def is_valid(self, address):
462 return self.wallet.is_valid(address)
464 class MiniDriver(QObject):
471 def __init__(self, wallet, window):
472 super(QObject, self).__init__()
477 self.wallet.register_callback(self.update_callback)
482 self.connect(self, SIGNAL("updatesignal()"), self.update)
484 # This is a hack to workaround that Qt does not like changing the
485 # window properties from this other thread before the runloop has
487 def update_callback(self):
488 self.emit(SIGNAL("updatesignal()"))
491 if not self.wallet.interface:
493 elif not self.wallet.interface.is_connected:
495 elif not self.wallet.blocks == -1:
497 elif not self.wallet.is_up_to_date:
502 if self.wallet.up_to_date:
503 self.update_balance()
504 self.update_completions()
506 def initializing(self):
507 if self.state == self.INITIALIZING:
509 self.state = self.INITIALIZING
510 self.window.deactivate()
512 def connecting(self):
513 if self.state == self.CONNECTING:
515 self.state = self.CONNECTING
516 self.window.deactivate()
518 def synchronizing(self):
519 if self.state == self.SYNCHRONIZING:
521 self.state = self.SYNCHRONIZING
522 self.window.deactivate()
525 if self.state == self.READY:
527 self.state = self.READY
528 self.window.activate()
530 def update_balance(self):
531 conf_balance, unconf_balance = self.wallet.get_balance()
532 balance = D(conf_balance + unconf_balance)
533 self.window.set_balances(balance)
535 def update_completions(self):
537 for addr, label in self.wallet.labels.items():
538 if addr in self.wallet.addressbook:
539 completions.append("%s <%s>" % (label, addr))
540 completions = completions + self.wallet.aliases.keys()
541 self.window.update_completions(completions)
543 if __name__ == "__main__":
544 app = QApplication(sys.argv)
545 with open("data/style.css") as style_file:
546 app.setStyleSheet(style_file.read())
548 sys.exit(app.exec_())