3 # Let's do some dep checking and handle missing ones gracefully
5 from PyQt4.QtCore import *
6 from PyQt4.QtGui import *
7 from PyQt4.Qt import Qt
8 import PyQt4.QtCore as QtCore
11 print "You need to have PyQT installed to run Electrum in graphical mode."
12 print "If you have pip installed try 'sudo pip install pyqt' if you are on Debian/Ubuntu try 'sudo apt-get install python-qt4'."
15 from decimal import Decimal as D
16 from electrum.util import get_resource_path as rsrc
17 from electrum.bitcoin import is_valid
26 from electrum.wallet import Wallet, WalletStorage
29 import receiving_widget
30 from electrum import util
34 from electrum.version import ELECTRUM_VERSION as electrum_version
35 from electrum.util import format_satoshis, age
37 from main_window import ElectrumWindow
42 bitcoin = lambda v: v * 100000000
44 def IconButton(filename, parent=None):
45 pixmap = QPixmap(filename)
47 return QPushButton(icon, "", parent)
52 self.emit(SIGNAL('timersignal'))
55 def resize_line_edit_width(line_edit, text_input):
56 metrics = QFontMetrics(qApp.font())
57 # Create an extra character to add some space on the end
59 line_edit.setMinimumWidth(metrics.width(text_input))
61 def load_theme_name(theme_path):
63 with open(os.path.join(theme_path, "name.cfg")) as name_cfg_file:
64 return name_cfg_file.read().rstrip("\n").strip()
69 def theme_dirs_from_prefix(prefix):
70 if not os.path.exists(prefix):
73 for potential_theme in os.listdir(prefix):
74 theme_full_path = os.path.join(prefix, potential_theme)
75 theme_css = os.path.join(theme_full_path, "style.css")
76 if not os.path.exists(theme_css):
78 theme_name = load_theme_name(theme_full_path)
79 if theme_name is None:
81 theme_paths[theme_name] = prefix, potential_theme
84 def load_theme_paths():
86 prefixes = (util.local_data_dir(), util.appdata_dir())
87 for prefix in prefixes:
88 theme_paths.update(theme_dirs_from_prefix(prefix))
92 def csv_transaction(wallet):
94 select_export = _('Select file to export your wallet transactions to')
95 fileName = QFileDialog.getSaveFileName(QWidget(), select_export, os.path.expanduser('~/electrum-history.csv'), "*.csv")
97 with open(fileName, "w+") as csvfile:
98 transaction = csv.writer(csvfile)
99 transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"])
100 for item in wallet.get_tx_history():
101 tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item
103 if timestamp is not None:
105 time_string = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
106 except [RuntimeError, TypeError, NameError] as reason:
107 time_string = "unknown"
110 time_string = "unknown"
112 time_string = "pending"
114 if value is not None:
115 value_string = format_satoshis(value, True, wallet.num_zeros)
120 fee_string = format_satoshis(fee, True, wallet.num_zeros)
125 label, is_default_label = wallet.get_label(tx_hash)
129 balance_string = format_satoshis(balance, False, wallet.num_zeros)
130 transaction.writerow([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string])
131 QMessageBox.information(None,"CSV Export created", "Your CSV export has been successfully created.")
132 except (IOError, os.error), reason:
133 export_error_label = _("Electrum was unable to produce a transaction export.")
134 QMessageBox.critical(None,"Unable to create csv", export_error_label + "\n" + str(reason))
137 class ElectrumGui(QObject):
139 def __init__(self, config, interface, blockchain, expert=None):
140 super(QObject, self).__init__()
143 self.check_qt_version()
145 self.interface = interface
146 self.blockchain = blockchain
151 def check_qt_version(self):
152 qtVersion = qVersion()
153 if not(int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
154 app = QApplication(sys.argv)
155 QMessageBox.warning(None,"Could not start Lite GUI.", "Electrum was unable to load the 'Lite GUI' because it needs Qt version >= 4.7.\nChanging your config to use the 'Classic' GUI")
156 self.config.set_key('gui','classic',True)
162 if self.expert is None:
163 self.app = QApplication(sys.argv)
164 storage = WalletStorage(self.config)
165 if not storage.file_exists:
167 self.wallet = Wallet(storage)
168 self.wallet.start_threads(self.interface, self.blockchain)
170 self.app = self.expert.app
171 self.wallet = self.expert.wallet
173 actuator = MiniActuator(self.config, self.wallet)
174 # Should probably not modify the current path but instead
175 # change the behaviour of rsrc(...)
176 old_path = QDir.currentPath()
177 actuator.load_theme()
179 self.mini = MiniWindow(actuator, self.expand, self.config)
180 driver = MiniDriver(self.wallet, self.mini)
182 # Reset path back to original value now that loading the GUI
184 QDir.setCurrent(old_path)
189 if self.expert is None:
190 self.expert = ElectrumWindow(self.config)
191 self.expert.load_wallet(self.wallet)
192 self.expert.app = self.app
195 self.expert.connect_slots(timer)
196 self.expert.update_wallet()
202 """Hide the lite mode window and show pro-mode."""
203 self.config.set_key('gui', 'classic', True)
207 def set_url(self, url):
208 payto, amount, label, message, signature, identity, url = \
209 self.wallet.parse_url(url, self.show_message, self.show_question)
210 self.mini.set_payment_fields(payto, amount)
212 def show_message(self, message):
213 QMessageBox.information(self.mini, _("Message"), message, _("OK"))
215 def show_question(self, message):
216 choice = QMessageBox.question(self.mini, _("Message"), message,
217 QMessageBox.Yes|QMessageBox.No,
219 return choice == QMessageBox.Yes
221 def restore_or_create(self):
222 qt_gui_object = ElectrumGui(self.wallet, self.app)
223 return qt_gui_object.restore_or_create()
225 class TransactionWindow(QDialog):
228 label = unicode(self.label_edit.text())
229 self.parent.wallet.labels[self.tx_id] = label
231 super(TransactionWindow, self).accept()
233 def __init__(self, transaction_id, parent):
234 super(TransactionWindow, self).__init__()
236 self.tx_id = str(transaction_id)
241 self.setWindowTitle(_("Transaction successfully sent"))
243 self.layout = QGridLayout(self)
244 history_label = "%s\n%s" % (_("Your transaction has been sent."), _("Please enter a label for this transaction for future reference."))
245 self.layout.addWidget(QLabel(history_label))
247 self.label_edit = QLineEdit()
248 self.label_edit.setPlaceholderText(_("Transaction label"))
249 self.label_edit.setObjectName("label_input")
250 self.label_edit.setAttribute(Qt.WA_MacShowFocusRect, 0)
251 self.label_edit.setFocusPolicy(Qt.ClickFocus)
252 self.layout.addWidget(self.label_edit)
254 self.save_button = QPushButton(_("Save"))
255 self.layout.addWidget(self.save_button)
256 self.save_button.clicked.connect(self.set_label)
260 class MiniWindow(QDialog):
262 def __init__(self, actuator, expand_callback, config):
263 super(MiniWindow, self).__init__()
264 tx = "e08115d0f7819aee65b9d24f81ef9d46eb62bb67ddef5318156cbc3ceb7b703e"
266 self.actuator = actuator
268 self.btc_balance = None
269 self.quote_currencies = ["BRL", "CNY", "EUR", "GBP", "RUB", "USD"]
270 self.actuator.set_configured_currency(self.set_quote_currency)
271 self.exchanger = exchange_rate.Exchanger(self)
272 # Needed because price discovery is done in a different thread
273 # which needs to be sent back to this main one to update the GUI
274 self.connect(self, SIGNAL("refresh_balance()"), self.refresh_balance)
276 self.balance_label = BalanceLabel(self.change_quote_currency)
277 self.balance_label.setObjectName("balance_label")
280 # Bitcoin address code
281 self.address_input = QLineEdit()
282 self.address_input.setPlaceholderText(_("Enter a Bitcoin address or contact"))
283 self.address_input.setObjectName("address_input")
285 self.address_input.setFocusPolicy(Qt.ClickFocus)
287 self.address_input.textChanged.connect(self.address_field_changed)
288 resize_line_edit_width(self.address_input,
289 "1BtaFUr3qVvAmwrsuDuu5zk6e4s2rxd2Gy")
291 self.address_completions = QStringListModel()
292 address_completer = QCompleter(self.address_input)
293 address_completer.setCaseSensitivity(False)
294 address_completer.setModel(self.address_completions)
295 self.address_input.setCompleter(address_completer)
297 address_layout = QHBoxLayout()
298 address_layout.addWidget(self.address_input)
300 self.amount_input = QLineEdit()
301 self.amount_input.setPlaceholderText(_("... and amount"))
302 self.amount_input.setObjectName("amount_input")
304 self.amount_input.setFocusPolicy(Qt.ClickFocus)
305 # This is changed according to the user's displayed balance
306 self.amount_validator = QDoubleValidator(self.amount_input)
307 self.amount_validator.setNotation(QDoubleValidator.StandardNotation)
308 self.amount_validator.setDecimals(8)
309 self.amount_input.setValidator(self.amount_validator)
311 # This removes the very ugly OSX highlighting, please leave this in :D
312 self.address_input.setAttribute(Qt.WA_MacShowFocusRect, 0)
313 self.amount_input.setAttribute(Qt.WA_MacShowFocusRect, 0)
314 self.amount_input.textChanged.connect(self.amount_input_changed)
316 if self.actuator.wallet.seed:
317 self.send_button = QPushButton(_("&Send"))
319 self.send_button = QPushButton(_("&Create"))
321 self.send_button.setObjectName("send_button")
322 self.send_button.setDisabled(True);
323 self.send_button.clicked.connect(self.send)
325 # Creating the receive button
326 self.switch_button = QPushButton( QIcon(":icons/switchgui.png"),'' )
327 self.switch_button.setMaximumWidth(25)
328 self.switch_button.setFlat(True)
329 self.switch_button.clicked.connect(expand_callback)
331 main_layout = QGridLayout(self)
333 main_layout.addWidget(self.balance_label, 0, 0, 1, 3)
334 main_layout.addWidget(self.switch_button, 0, 3)
336 main_layout.addWidget(self.address_input, 1, 0, 1, 4)
337 main_layout.addWidget(self.amount_input, 2, 0, 1, 2)
338 main_layout.addWidget(self.send_button, 2, 2, 1, 2)
340 self.send_button.setMaximumWidth(125)
342 self.history_list = history_widget.HistoryWidget()
343 self.history_list.setObjectName("history")
344 self.history_list.hide()
345 self.history_list.setAlternatingRowColors(True)
347 main_layout.addWidget(self.history_list, 3, 0, 1, 4)
349 self.receiving = receiving_widget.ReceivingWidget(self)
350 self.receiving.setObjectName("receiving")
352 # Add to the right side
353 self.receiving_box = QGroupBox(_("Select a receiving address"))
354 extra_layout = QGridLayout()
356 # Checkbox to filter used addresses
357 hide_used = QCheckBox(_('Hide used addresses'))
358 hide_used.setChecked(True)
359 hide_used.stateChanged.connect(self.receiving.toggle_used)
361 # Events for receiving addresses
362 self.receiving.clicked.connect(self.receiving.copy_address)
363 self.receiving.itemDoubleClicked.connect(self.receiving.edit_label)
364 self.receiving.itemChanged.connect(self.receiving.update_label)
368 extra_layout.addWidget( QLabel(_('Selecting an address will copy it to the clipboard.') + '\n' + _('Double clicking the label will allow you to edit it.') ),0,0)
370 extra_layout.addWidget(self.receiving, 1,0)
371 extra_layout.addWidget(hide_used, 2,0)
372 extra_layout.setColumnMinimumWidth(0,200)
374 self.receiving_box.setLayout(extra_layout)
375 main_layout.addWidget(self.receiving_box,0,4,-1,3)
376 self.receiving_box.hide()
378 # Creating the menu bar
380 electrum_menu = menubar.addMenu(_("&Electrum"))
382 quit_option = electrum_menu.addAction(_("&Close"))
384 quit_option.triggered.connect(self.close)
386 view_menu = menubar.addMenu(_("&View"))
387 extra_menu = menubar.addMenu(_("&Extra"))
389 backup_wallet_menu = extra_menu.addAction( _("&Create wallet backup"))
390 backup_wallet_menu.triggered.connect(lambda: backup_wallet(self.config.path))
392 export_csv = extra_menu.addAction( _("&Export transactions to CSV") )
393 export_csv.triggered.connect(lambda: csv_transaction(self.actuator.wallet))
395 master_key = extra_menu.addAction( _("Copy master public key to clipboard") )
396 master_key.triggered.connect(self.actuator.copy_master_public_key)
398 expert_gui = view_menu.addAction(_("&Classic GUI"))
399 expert_gui.triggered.connect(expand_callback)
400 themes_menu = view_menu.addMenu(_("&Themes"))
401 selected_theme = self.actuator.selected_theme()
402 theme_group = QActionGroup(self)
403 for theme_name in self.actuator.theme_names():
404 theme_action = themes_menu.addAction(theme_name)
405 theme_action.setCheckable(True)
406 if selected_theme == theme_name:
407 theme_action.setChecked(True)
408 class SelectThemeFunctor:
409 def __init__(self, theme_name, toggle_theme):
410 self.theme_name = theme_name
411 self.toggle_theme = toggle_theme
412 def __call__(self, checked):
414 self.toggle_theme(self.theme_name)
415 delegate = SelectThemeFunctor(theme_name, self.toggle_theme)
416 theme_action.toggled.connect(delegate)
417 theme_group.addAction(theme_action)
418 view_menu.addSeparator()
420 show_receiving = view_menu.addAction(_("Show Receiving addresses"))
421 show_receiving.setCheckable(True)
422 show_receiving.toggled.connect(self.toggle_receiving_layout)
424 show_receiving_toggle = self.config.get("gui_show_receiving",False)
425 show_receiving.setChecked(show_receiving_toggle)
426 self.show_receiving = show_receiving
428 self.toggle_receiving_layout(show_receiving_toggle)
431 show_history = view_menu.addAction(_("Show History"))
432 show_history.setCheckable(True)
433 show_history.toggled.connect(self.show_history)
435 help_menu = menubar.addMenu(_("&Help"))
436 the_website = help_menu.addAction(_("&Website"))
437 the_website.triggered.connect(self.the_website)
438 help_menu.addSeparator()
439 report_bug = help_menu.addAction(_("&Report Bug"))
440 report_bug.triggered.connect(self.show_report_bug)
441 show_about = help_menu.addAction(_("&About"))
442 show_about.triggered.connect(self.show_about)
443 main_layout.setMenuBar(menubar)
444 self.main_layout = main_layout
446 quit_shortcut = QShortcut(QKeySequence("Ctrl+Q"), self)
447 quit_shortcut.activated.connect(self.close)
448 close_shortcut = QShortcut(QKeySequence("Ctrl+W"), self)
449 close_shortcut.activated.connect(self.close)
451 g = self.config.get("winpos-lite",[4, 25, 351, 149])
452 self.setGeometry(g[0], g[1], g[2], g[3])
454 show_hist = self.config.get("gui_show_history",False)
455 show_history.setChecked(show_hist)
456 self.show_history(show_hist)
458 self.setWindowIcon(QIcon(":icons/electrum.png"))
459 self.setWindowTitle("Electrum")
460 self.setWindowFlags(Qt.Window|Qt.MSWindowsFixedSizeDialogHint)
461 self.layout().setSizeConstraint(QLayout.SetFixedSize)
462 self.setObjectName("main_window")
466 def toggle_theme(self, theme_name):
467 old_path = QDir.currentPath()
468 self.actuator.change_theme(theme_name)
469 # Recompute style globally
470 qApp.style().unpolish(self)
471 qApp.style().polish(self)
472 QDir.setCurrent(old_path)
474 def closeEvent(self, event):
476 self.config.set_key("winpos-lite", [g.left(),g.top(),g.width(),g.height()],True)
477 self.config.set_key("gui_show_history", self.history_list.isVisible(),True)
478 self.config.set_key("gui_show_receiving", self.receiving_box.isVisible(),True)
480 super(MiniWindow, self).closeEvent(event)
483 def set_payment_fields(self, dest_address, amount):
484 self.address_input.setText(dest_address)
485 self.address_field_changed(dest_address)
486 self.amount_input.setText(amount)
491 def deactivate(self):
494 def set_quote_currency(self, currency):
495 """Set and display the fiat currency country."""
496 if currency not in self.quote_currencies:
498 self.quote_currencies.remove(currency)
499 self.quote_currencies.insert(0, currency)
500 self.refresh_balance()
502 def change_quote_currency(self, forward=True):
504 self.quote_currencies = \
505 self.quote_currencies[1:] + self.quote_currencies[0:1]
507 self.quote_currencies = \
508 self.quote_currencies[-1:] + self.quote_currencies[0:-1]
509 self.actuator.set_config_currency(self.quote_currencies[0])
510 self.refresh_balance()
512 def refresh_balance(self):
513 if self.btc_balance is None:
514 # Price has been discovered before wallet has been loaded
515 # and server connect... so bail.
517 self.set_balances(self.btc_balance)
518 self.amount_input_changed(self.amount_input.text())
520 def set_balances(self, btc_balance):
521 """Set the bitcoin balance and update the amount label accordingly."""
522 self.btc_balance = btc_balance
523 quote_text = self.create_quote_text(btc_balance)
525 quote_text = "(%s)" % quote_text
526 btc_balance = "%.4f" % (btc_balance / bitcoin(1))
527 self.balance_label.set_balance_text(btc_balance, quote_text)
528 self.setWindowTitle("Electrum %s - %s BTC" % (electrum_version, btc_balance))
530 def amount_input_changed(self, amount_text):
531 """Update the number of bitcoins displayed."""
532 self.check_button_status()
535 amount = D(str(amount_text))
536 except decimal.InvalidOperation:
537 self.balance_label.show_balance()
539 quote_text = self.create_quote_text(amount * bitcoin(1))
541 self.balance_label.set_amount_text(quote_text)
542 self.balance_label.show_amount()
544 self.balance_label.show_balance()
546 def create_quote_text(self, btc_balance):
547 """Return a string copy of the amount fiat currency the
548 user has in bitcoins."""
549 quote_currency = self.quote_currencies[0]
550 quote_balance = self.exchanger.exchange(btc_balance, quote_currency)
551 if quote_balance is None:
554 quote_text = "%.2f %s" % ((quote_balance / bitcoin(1)),
559 if self.actuator.send(self.address_input.text(),
560 self.amount_input.text(), self):
561 self.address_input.setText("")
562 self.amount_input.setText("")
564 def check_button_status(self):
565 """Check that the bitcoin address is valid and that something
566 is entered in the amount before making the send button clickable."""
568 value = D(str(self.amount_input.text())) * 10**8
569 except decimal.InvalidOperation:
571 # self.address_input.property(...) returns a qVariant, not a bool.
572 # The == is needed to properly invoke a comparison.
573 if (self.address_input.property("isValid") == True and
574 value is not None and 0 < value <= self.btc_balance):
575 self.send_button.setDisabled(False)
577 self.send_button.setDisabled(True)
579 def address_field_changed(self, address):
580 # label or alias, with address in brackets
581 match2 = re.match("(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>",
584 address = match2.group(2)
585 self.address_input.setText(address)
587 if is_valid(address):
588 self.check_button_status()
589 self.address_input.setProperty("isValid", True)
590 self.recompute_style(self.address_input)
592 self.send_button.setDisabled(True)
593 self.address_input.setProperty("isValid", False)
594 self.recompute_style(self.address_input)
596 if len(address) == 0:
597 self.address_input.setProperty("isValid", None)
598 self.recompute_style(self.address_input)
600 def recompute_style(self, element):
601 self.style().unpolish(element)
602 self.style().polish(element)
604 def copy_address(self):
605 receive_popup = ReceivePopup(self.receive_button)
606 self.actuator.copy_address(receive_popup)
608 def update_completions(self, completions):
609 self.address_completions.setStringList(completions)
612 def update_history(self, tx_history):
614 self.history_list.empty()
616 for item in tx_history[-10:]:
617 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
618 label = self.actuator.wallet.get_label(tx_hash)[0]
619 #amount = D(value) / 10**8
620 v_str = format_satoshis(value, True)
621 self.history_list.append(label, v_str, age(timestamp))
624 self.actuator.acceptbit(self.quote_currencies[0])
626 def the_website(self):
627 webbrowser.open("http://electrum.org")
629 def show_about(self):
630 QMessageBox.about(self, "Electrum",
631 _("Version")+" %s" % (electrum_version) + "\n\n" + _("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 conjunction with high-performance servers that handle the most complicated parts of the Bitcoin system."))
633 def show_report_bug(self):
634 QMessageBox.information(self, "Electrum - " + _("Reporting Bugs"),
635 _("Please report any bugs as issues on github:")+" <a href=\"https://github.com/spesmilo/electrum/issues\">https://github.com/spesmilo/electrum/issues</a>")
637 def toggle_receiving_layout(self, toggle_state):
639 self.receiving_box.show()
641 self.receiving_box.hide()
643 def show_history(self, toggle_state):
645 self.main_layout.setRowMinimumHeight(3,200)
646 self.history_list.show()
648 self.main_layout.setRowMinimumHeight(3,0)
649 self.history_list.hide()
651 class BalanceLabel(QLabel):
657 def __init__(self, change_quote_currency, parent=None):
658 super(QLabel, self).__init__(_("Connecting..."), parent)
659 self.change_quote_currency = change_quote_currency
660 self.state = self.SHOW_CONNECTING
661 self.balance_text = ""
662 self.amount_text = ""
664 def mousePressEvent(self, event):
665 """Change the fiat currency selection if window background is clicked."""
666 if self.state != self.SHOW_CONNECTING:
667 self.change_quote_currency(event.button() == Qt.LeftButton)
669 def set_balance_text(self, btc_balance, quote_text):
670 """Set the amount of bitcoins in the gui."""
671 if self.state == self.SHOW_CONNECTING:
672 self.state = self.SHOW_BALANCE
673 self.balance_text = "<span style='font-size: 18pt'>%s</span> <span style='font-size: 10pt'>BTC</span> <span style='font-size: 10pt'>%s</span>" % (btc_balance, quote_text)
674 if self.state == self.SHOW_BALANCE:
675 self.setText(self.balance_text)
677 def set_amount_text(self, quote_text):
678 self.amount_text = "<span style='font-size: 10pt'>%s</span>" % quote_text
679 if self.state == self.SHOW_AMOUNT:
680 self.setText(self.amount_text)
682 def show_balance(self):
683 if self.state == self.SHOW_AMOUNT:
684 self.state = self.SHOW_BALANCE
685 self.setText(self.balance_text)
687 def show_amount(self):
688 if self.state == self.SHOW_BALANCE:
689 self.state = self.SHOW_AMOUNT
690 self.setText(self.amount_text)
692 def ok_cancel_buttons(dialog):
693 row_layout = QHBoxLayout()
694 row_layout.addStretch(1)
695 ok_button = QPushButton(_("OK"))
696 row_layout.addWidget(ok_button)
697 ok_button.clicked.connect(dialog.accept)
698 cancel_button = QPushButton(_("Cancel"))
699 row_layout.addWidget(cancel_button)
700 cancel_button.clicked.connect(dialog.reject)
703 class PasswordDialog(QDialog):
705 def __init__(self, parent):
706 super(QDialog, self).__init__(parent)
710 self.password_input = QLineEdit()
711 self.password_input.setEchoMode(QLineEdit.Password)
713 main_layout = QVBoxLayout(self)
714 message = _('Please enter your password')
715 main_layout.addWidget(QLabel(message))
719 grid.addWidget(QLabel(_('Password')), 1, 0)
720 grid.addWidget(self.password_input, 1, 1)
721 main_layout.addLayout(grid)
723 main_layout.addLayout(ok_cancel_buttons(self))
724 self.setLayout(main_layout)
729 return unicode(self.password_input.text())
731 class ReceivePopup(QDialog):
733 def leaveEvent(self, event):
736 def setup(self, address):
737 label = QLabel(_("Copied your Bitcoin address to the clipboard!"))
738 address_display = QLineEdit(address)
739 address_display.setReadOnly(True)
740 resize_line_edit_width(address_display, address)
742 main_layout = QVBoxLayout(self)
743 main_layout.addWidget(label)
744 main_layout.addWidget(address_display)
746 self.setMouseTracking(True)
747 self.setWindowTitle("Electrum - " + _("Receive Bitcoin payment"))
748 self.setWindowFlags(Qt.Window|Qt.FramelessWindowHint|
749 Qt.MSWindowsFixedSizeDialogHint)
750 self.layout().setSizeConstraint(QLayout.SetFixedSize)
751 #self.setFrameStyle(QFrame.WinPanel|QFrame.Raised)
752 #self.setAlignment(Qt.AlignCenter)
755 parent = self.parent()
756 top_left_pos = parent.mapToGlobal(parent.rect().bottomLeft())
757 self.move(top_left_pos)
758 center_mouse_pos = self.mapToGlobal(self.rect().center())
759 QCursor.setPos(center_mouse_pos)
763 """Initialize the definitions relating to themes and
764 sending/receiving bitcoins."""
767 def __init__(self, config, wallet):
768 """Retrieve the gui theme used in previous session."""
771 self.theme_name = self.config.get('litegui_theme','Cleanlook')
772 self.themes = load_theme_paths()
774 def load_theme(self):
775 """Load theme retrieved from wallet file."""
777 theme_prefix, theme_path = self.themes[self.theme_name]
779 util.print_error("Theme not found!", self.theme_name)
781 QDir.setCurrent(os.path.join(theme_prefix, theme_path))
782 with open(rsrc("style.css")) as style_file:
783 qApp.setStyleSheet(style_file.read())
785 def theme_names(self):
787 return sorted(self.themes.keys())
789 def selected_theme(self):
791 return self.theme_name
793 def change_theme(self, theme_name):
795 self.theme_name = theme_name
796 self.config.set_key('litegui_theme',theme_name)
799 def set_configured_currency(self, set_quote_currency):
800 """Set the inital fiat currency conversion country (USD/EUR/GBP) in
801 the GUI to what it was set to in the wallet."""
802 currency = self.config.get('currency')
803 # currency can be none when Electrum is used for the first
804 # time and no setting has been created yet.
805 if currency is not None:
806 set_quote_currency(currency)
808 def set_config_currency(self, conversion_currency):
809 """Change the wallet fiat currency country."""
810 self.config.set_key('conversion_currency',conversion_currency,True)
812 def copy_address(self, receive_popup):
813 """Copy the wallet addresses into the client."""
814 addrs = [addr for addr in self.wallet.addresses(True)
815 if not self.wallet.is_change(addr)]
816 # Select most recent addresses from gap limit
817 addrs = addrs[-self.wallet.gap_limit:]
818 copied_address = random.choice(addrs)
819 qApp.clipboard().setText(copied_address)
820 receive_popup.setup(copied_address)
821 receive_popup.popup()
823 def waiting_dialog(self, f):
828 w.setWindowTitle('Electrum')
829 l = QLabel('Sending transaction, please wait.')
838 w.connect(s, QtCore.SIGNAL('timersignal'), ff)
843 def send(self, address, amount, parent_window):
844 """Send bitcoins to the target address."""
845 dest_address = self.fetch_destination(address)
847 if dest_address is None or not is_valid(dest_address):
848 QMessageBox.warning(parent_window, _('Error'),
849 _('Invalid Bitcoin Address') + ':\n' + address, _('OK'))
852 convert_amount = lambda amount: \
853 int(D(unicode(amount)) * bitcoin(1))
854 amount = convert_amount(amount)
856 if self.wallet.use_encryption:
857 password_dialog = PasswordDialog(parent_window)
858 password = password_dialog.run()
866 if amount < bitcoin(1) / 10:
868 fee = bitcoin(1) / 1000
871 tx = self.wallet.mktx([(dest_address, amount)], password, fee)
872 except BaseException as error:
873 QMessageBox.warning(parent_window, _('Error'), str(error), _('OK'))
877 h = self.wallet.send_tx(tx)
879 self.waiting_dialog(lambda: False if self.wallet.tx_event.isSet() else _("Sending transaction, please wait..."))
881 status, message = self.wallet.receive_tx(h)
885 dumpf = tempfile.NamedTemporaryFile(delete=False)
888 print "Dumped error tx to", dumpf.name
889 QMessageBox.warning(parent_window, _('Error'), message, _('OK'))
892 TransactionWindow(message, self)
894 filename = 'unsigned_tx_%s' % (time.mktime(time.gmtime()))
896 fileName = QFileDialog.getSaveFileName(QWidget(), _("Select a transaction filename"), os.path.expanduser('~/%s' % (filename)))
897 with open(fileName,'w') as f:
898 f.write(json.dumps(tx.as_dict(),indent=4) + '\n')
899 QMessageBox.information(QWidget(), _('Unsigned transaction created'), _("Unsigned transaction was saved to file:") + " " +fileName, _('OK'))
900 except BaseException as e:
901 QMessageBox.warning(QWidget(), _('Error'), _('Could not write transaction to file: %s' % e), _('OK'))
904 def fetch_destination(self, address):
905 recipient = unicode(address).strip()
908 match1 = re.match("^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$",
911 # label or alias, with address in brackets
912 match2 = re.match("(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>",
917 self.wallet.get_alias(recipient, True,
918 self.show_message, self.question)
921 return match2.group(2)
926 def copy_master_public_key(self):
927 master_pubkey = self.wallet.get_master_public_key()
928 qApp.clipboard().setText(master_pubkey)
929 QMessageBox.information(None, _("Copy successful"), _("Your master public key has been copied to your clipboard."))
932 def acceptbit(self, currency):
933 master_pubkey = self.wallet.master_public_key
934 url = "http://acceptbit.com/mpk/%s/%s" % (master_pubkey, currency)
937 def show_seed_dialog(self):
938 ElectrumWindow.show_seed_dialog(self.wallet)
940 class MiniDriver(QObject):
947 def __init__(self, wallet, window):
948 super(QObject, self).__init__()
953 self.wallet.network.register_callback('updated',self.update_callback)
954 self.wallet.network.register_callback('connected', self.update_callback)
955 self.wallet.network.register_callback('disconnected', self.update_callback)
960 self.connect(self, SIGNAL("updatesignal()"), self.update)
961 self.update_callback()
963 # This is a hack to workaround that Qt does not like changing the
964 # window properties from this other thread before the runloop has
966 def update_callback(self):
967 self.emit(SIGNAL("updatesignal()"))
970 if not self.wallet.interface:
972 elif not self.wallet.interface.is_connected:
974 elif not self.wallet.up_to_date:
979 if self.wallet.up_to_date:
980 self.update_balance()
981 self.update_completions()
982 self.update_history()
984 def initializing(self):
985 if self.state == self.INITIALIZING:
987 self.state = self.INITIALIZING
988 self.window.deactivate()
990 def connecting(self):
991 if self.state == self.CONNECTING:
993 self.state = self.CONNECTING
994 self.window.deactivate()
996 def synchronizing(self):
997 if self.state == self.SYNCHRONIZING:
999 self.state = self.SYNCHRONIZING
1000 self.window.deactivate()
1003 if self.state == self.READY:
1005 self.state = self.READY
1006 self.window.activate()
1008 def update_balance(self):
1009 conf_balance, unconf_balance = self.wallet.get_balance()
1010 balance = D(conf_balance + unconf_balance)
1011 self.window.set_balances(balance)
1013 def update_completions(self):
1015 for addr, label in self.wallet.labels.items():
1016 if addr in self.wallet.addressbook:
1017 completions.append("%s <%s>" % (label, addr))
1018 self.window.update_completions(completions)
1020 def update_history(self):
1021 tx_history = self.wallet.get_tx_history()
1022 self.window.update_history(tx_history)
1025 if __name__ == "__main__":
1026 app = QApplication(sys.argv)
1027 with open(rsrc("style.css")) as style_file:
1028 app.setStyleSheet(style_file.read())
1030 sys.exit(app.exec_())