19bad2b945768d563e4a275e9eeb3e4abc723161
[electrum-nvc.git] / lib / gui_lite.py
1 from PyQt4.QtCore import *
2 from PyQt4.QtGui import *
3 from decimal import Decimal as D
4 from util import appdata_dir, get_resource_path as rsrc
5 from i18n import _
6 import decimal
7 import exchange_rate
8 import os.path
9 import random
10 import re
11 import sys
12 import time
13 import wallet
14
15 try:
16     import lib.gui_qt as gui_qt
17 except ImportError:
18     import electrum.gui_qt as gui_qt
19
20 bitcoin = lambda v: v * 100000000
21
22 def IconButton(filename, parent=None):
23     pixmap = QPixmap(filename)
24     icon = QIcon(pixmap)
25     return QPushButton(icon, "", parent)
26
27 class Timer(QThread):
28     def run(self):
29         while True:
30             self.emit(SIGNAL('timersignal'))
31             time.sleep(0.5)
32
33 def resize_line_edit_width(line_edit, text_input):
34     metrics = QFontMetrics(qApp.font())
35     # Create an extra character to add some space on the end
36     text_input += "A"
37     line_edit.setMinimumWidth(metrics.width(text_input))
38
39 class ElectrumGui:
40
41     def __init__(self, wallet):
42         self.wallet = wallet
43         self.app = QApplication(sys.argv)
44         if os.path.exists("data"):
45             QDir.setCurrent("data")
46         else:
47             QDir.setCurrent(appdata_dir())
48         with open(rsrc("style.css")) as style_file:
49             self.app.setStyleSheet(style_file.read())
50
51     def main(self, url):
52         actuator = MiniActuator(self.wallet)
53         self.mini = MiniWindow(actuator, self.expand)
54         driver = MiniDriver(self.wallet, self.mini)
55
56         if url:
57             self.set_url(url)
58
59         timer = Timer()
60         timer.start()
61         self.expert = gui_qt.ElectrumWindow(self.wallet)
62         self.expert.app = self.app
63         self.expert.connect_slots(timer)
64         self.expert.update_wallet()
65
66         sys.exit(self.app.exec_())
67
68     def expand(self):
69         self.mini.hide()
70         self.expert.show()
71
72     def set_url(self, url):
73         payto, amount, label, message, signature, identity, url = \
74             self.wallet.parse_url(url, self.show_message, self.show_question)
75         self.mini.set_payment_fields(payto, amount)
76
77     def show_message(self, message):
78         QMessageBox.information(self.mini, _("Message"), message, _("OK"))
79
80     def show_question(self, message):
81         choice = QMessageBox.question(self.mini, _("Message"), message,
82                                       QMessageBox.Yes|QMessageBox.No,
83                                       QMessageBox.No)
84         return choice == QMessageBox.Yes
85
86     def restore_or_create(self):
87         qt_gui_object = gui_qt.ElectrumGui(self.wallet, self.app)
88         return qt_gui_object.restore_or_create()
89
90 class MiniWindow(QDialog):
91
92     def __init__(self, actuator, expand_callback):
93         super(MiniWindow, self).__init__()
94
95         self.actuator = actuator
96
97         accounts_button = IconButton(rsrc("icons", "accounts.png"))
98         accounts_button.setObjectName("accounts_button")
99
100         self.accounts_selector = QMenu()
101         accounts_button.setMenu(self.accounts_selector)
102
103         interact_button = IconButton(rsrc("icons", "interact.png"))
104         interact_button.setObjectName("interact_button")
105
106         app_menu = QMenu(interact_button)
107         report_action = app_menu.addAction(_("&Report Bug"))
108         about_action = app_menu.addAction(_("&About Electrum"))
109         app_menu.addSeparator()
110         quit_action = app_menu.addAction(_("&Quit"))
111         interact_button.setMenu(app_menu)
112
113         self.connect(report_action, SIGNAL("triggered()"),
114                      self.show_report_bug)
115         self.connect(about_action, SIGNAL("triggered()"), self.show_about)
116         self.connect(quit_action, SIGNAL("triggered()"), self.close)
117
118         expand_button = IconButton(rsrc("icons", "expand.png"))
119         expand_button.setObjectName("expand_button")
120         self.connect(expand_button, SIGNAL("clicked()"), expand_callback)
121
122         self.btc_balance = None
123         self.quote_currencies = ("EUR", "USD", "GBP")
124         self.exchanger = exchange_rate.Exchanger(self)
125         # Needed because price discovery is done in a different thread
126         # which needs to be sent back to this main one to update the GUI
127         self.connect(self, SIGNAL("refresh_balance()"), self.refresh_balance)
128
129         self.balance_label = BalanceLabel(self.change_quote_currency)
130         self.balance_label.setObjectName("balance_label")
131
132         self.receive_button = QPushButton(_("&Receive"))
133         self.receive_button.setObjectName("receive_button")
134         self.receive_button.setDefault(True)
135         self.connect(self.receive_button, SIGNAL("clicked()"),
136                      self.copy_address)
137
138         self.address_input = TextedLineEdit(_("Enter a Bitcoin address..."))
139         self.address_input.setObjectName("address_input")
140         self.connect(self.address_input, SIGNAL("textEdited(QString)"),
141                      self.address_field_changed)
142         resize_line_edit_width(self.address_input,
143                                "1BtaFUr3qVvAmwrsuDuu5zk6e4s2rxd2Gy")
144
145         self.address_completions = QStringListModel()
146         address_completer = QCompleter(self.address_input)
147         address_completer.setCaseSensitivity(False)
148         address_completer.setModel(self.address_completions)
149         self.address_input.setCompleter(address_completer)
150
151         self.valid_address = QCheckBox()
152         self.valid_address.setObjectName("valid_address")
153         self.valid_address.setEnabled(False)
154         self.valid_address.setChecked(False)
155
156         address_layout = QHBoxLayout()
157         address_layout.addWidget(self.address_input)
158         address_layout.addWidget(self.valid_address)
159
160         self.amount_input = TextedLineEdit(_("... and amount"))
161         self.amount_input.setObjectName("amount_input")
162         # This is changed according to the user's displayed balance
163         self.amount_validator = QDoubleValidator(self.amount_input)
164         self.amount_validator.setNotation(QDoubleValidator.StandardNotation)
165         self.amount_validator.setDecimals(8)
166         self.amount_input.setValidator(self.amount_validator)
167
168         self.connect(self.amount_input, SIGNAL("textChanged(QString)"),
169                      self.amount_input_changed)
170
171         amount_layout = QHBoxLayout()
172         amount_layout.addWidget(self.amount_input)
173         amount_layout.addStretch()
174
175         send_button = QPushButton(_("&Send"))
176         send_button.setObjectName("send_button")
177         self.connect(send_button, SIGNAL("clicked()"), self.send)
178
179         main_layout = QGridLayout(self)
180         main_layout.addWidget(accounts_button, 0, 0)
181         main_layout.addWidget(interact_button, 1, 0)
182         main_layout.addWidget(expand_button, 2, 0)
183
184         main_layout.addWidget(self.balance_label, 0, 1)
185         main_layout.addWidget(self.receive_button, 0, 2)
186
187         main_layout.addLayout(address_layout, 1, 1, 1, -1)
188
189         main_layout.addLayout(amount_layout, 2, 1)
190         main_layout.addWidget(send_button, 2, 2)
191
192         self.setWindowTitle("Electrum")
193         self.setWindowFlags(Qt.Window|Qt.MSWindowsFixedSizeDialogHint)
194         self.layout().setSizeConstraint(QLayout.SetFixedSize)
195         self.setObjectName("main_window")
196         self.show()
197
198     def closeEvent(self, event):
199         super(MiniWindow, self).closeEvent(event)
200         qApp.quit()
201
202     def set_payment_fields(self, dest_address, amount):
203         self.address_input.become_active()
204         self.address_input.setText(dest_address)
205         self.address_field_changed(dest_address)
206         self.amount_input.become_active()
207         self.amount_input.setText(amount)
208
209     def activate(self):
210         pass
211
212     def deactivate(self):
213         pass
214
215     def change_quote_currency(self):
216         self.quote_currencies = \
217             self.quote_currencies[1:] + self.quote_currencies[0:1]
218         self.refresh_balance()
219
220     def refresh_balance(self):
221         if self.btc_balance is None:
222             # Price has been discovered before wallet has been loaded
223             # and server connect... so bail.
224             return
225         self.set_balances(self.btc_balance)
226         self.amount_input_changed(self.amount_input.text())
227
228     def set_balances(self, btc_balance):
229         self.btc_balance = btc_balance
230         quote_text = self.create_quote_text(btc_balance)
231         if quote_text:
232             quote_text = "(%s)" % quote_text
233         btc_balance = "%.2f" % (btc_balance / bitcoin(1))
234         self.balance_label.set_balance_text(btc_balance, quote_text)
235         main_account_info = \
236             "Checking - %s BTC" % btc_balance
237         self.setWindowTitle("Electrum - %s" % main_account_info)
238         self.accounts_selector.clear()
239         self.accounts_selector.addAction("%s %s" % (main_account_info,
240                                                     quote_text))
241
242     def amount_input_changed(self, amount_text):
243         try:
244             amount = D(str(amount_text))
245         except decimal.InvalidOperation:
246             self.balance_label.show_balance()
247         else:
248             quote_text = self.create_quote_text(amount * bitcoin(1))
249             if quote_text:
250                 self.balance_label.set_amount_text(quote_text)
251                 self.balance_label.show_amount()
252             else:
253                 self.balance_label.show_balance()
254
255     def create_quote_text(self, btc_balance):
256         quote_currency = self.quote_currencies[0]
257         quote_balance = self.exchanger.exchange(btc_balance, quote_currency)
258         if quote_balance is None:
259             quote_text = ""
260         else:
261             quote_text = "%.2f %s" % ((quote_balance / bitcoin(1)),
262                                       quote_currency)
263         return quote_text
264
265     def send(self):
266         if self.actuator.send(self.address_input.text(),
267                               self.amount_input.text(), self):
268             self.address_input.become_inactive()
269             self.amount_input.become_inactive()
270
271     def address_field_changed(self, address):
272         if self.actuator.is_valid(address):
273             self.valid_address.setChecked(True)
274         else:
275             self.valid_address.setChecked(False)
276
277     def copy_address(self):
278         receive_popup = ReceivePopup(self.receive_button)
279         self.actuator.copy_address(receive_popup)
280
281     def update_completions(self, completions):
282         self.address_completions.setStringList(completions)
283
284     def show_about(self):
285         QMessageBox.about(self, "Electrum",
286             _("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."))
287
288     def show_report_bug(self):
289         QMessageBox.information(self, "Electrum - " + _("Reporting Bugs"),
290             _("Email bug reports to %s") % "genjix" + "@" + "riseup.net")
291
292 class BalanceLabel(QLabel):
293
294     SHOW_CONNECTING = 1
295     SHOW_BALANCE = 2
296     SHOW_AMOUNT = 3
297
298     def __init__(self, change_quote_currency, parent=None):
299         super(QLabel, self).__init__(_("Connecting..."), parent)
300         self.change_quote_currency = change_quote_currency
301         self.state = self.SHOW_CONNECTING
302         self.balance_text = ""
303         self.amount_text = ""
304
305     def mousePressEvent(self, event):
306         if self.state != self.SHOW_CONNECTING:
307             self.change_quote_currency()
308
309     def set_balance_text(self, btc_balance, quote_text):
310         if self.state == self.SHOW_CONNECTING:
311             self.state = self.SHOW_BALANCE
312         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)
313         if self.state == self.SHOW_BALANCE:
314             self.setText(self.balance_text)
315
316     def set_amount_text(self, quote_text):
317         self.amount_text = "<span style='font-size: 10pt'>%s</span>" % quote_text
318         if self.state == self.SHOW_AMOUNT:
319             self.setText(self.amount_text)
320
321     def show_balance(self):
322         if self.state == self.SHOW_AMOUNT:
323             self.state = self.SHOW_BALANCE
324             self.setText(self.balance_text)
325
326     def show_amount(self):
327         if self.state == self.SHOW_BALANCE:
328             self.state = self.SHOW_AMOUNT
329             self.setText(self.amount_text)
330
331 class TextedLineEdit(QLineEdit):
332
333     def __init__(self, inactive_text, parent=None):
334         super(QLineEdit, self).__init__(parent)
335         self.inactive_text = inactive_text
336         self.become_inactive()
337
338     def mousePressEvent(self, event):
339         if self.isReadOnly():
340             self.become_active()
341         QLineEdit.mousePressEvent(self, event)
342
343     def focusOutEvent(self, event):
344         if self.text() == "":
345             self.become_inactive()
346         QLineEdit.focusOutEvent(self, event)
347
348     def focusInEvent(self, event):
349         if self.isReadOnly():
350             self.become_active()
351         QLineEdit.focusInEvent(self, event)
352
353     def become_inactive(self):
354         self.setText(self.inactive_text)
355         self.setReadOnly(True)
356         self.recompute_style()
357
358     def become_active(self):
359         self.setText("")
360         self.setReadOnly(False)
361         self.recompute_style()
362
363     def recompute_style(self):
364         qApp.style().unpolish(self)
365         qApp.style().polish(self)
366         # also possible but more expensive:
367         #qApp.setStyleSheet(qApp.styleSheet())
368
369 def ok_cancel_buttons(dialog):
370     row_layout = QHBoxLayout()
371     row_layout.addStretch(1)
372     ok_button = QPushButton(_("OK"))
373     row_layout.addWidget(ok_button)
374     ok_button.clicked.connect(dialog.accept)
375     cancel_button = QPushButton(_("Cancel"))
376     row_layout.addWidget(cancel_button)
377     cancel_button.clicked.connect(dialog.reject)
378     return row_layout
379
380 class PasswordDialog(QDialog):
381
382     def __init__(self, parent):
383         super(QDialog, self).__init__(parent)
384
385         self.setModal(True)
386
387         self.password_input = QLineEdit()
388         self.password_input.setEchoMode(QLineEdit.Password)
389
390         main_layout = QVBoxLayout(self)
391         message = _('Please enter your password')
392         main_layout.addWidget(QLabel(message))
393
394         grid = QGridLayout()
395         grid.setSpacing(8)
396         grid.addWidget(QLabel(_('Password')), 1, 0)
397         grid.addWidget(self.password_input, 1, 1)
398         main_layout.addLayout(grid)
399
400         main_layout.addLayout(ok_cancel_buttons(self))
401         self.setLayout(main_layout) 
402
403     def run(self):
404         if not self.exec_():
405             return
406         return unicode(self.password_input.text())
407
408 class ReceivePopup(QDialog):
409
410     def leaveEvent(self, event):
411         self.close()
412
413     def setup(self, address):
414         label = QLabel(_("Copied your Bitcoin address to the clipboard!"))
415         address_display = QLineEdit(address)
416         address_display.setReadOnly(True)
417         resize_line_edit_width(address_display, address)
418
419         main_layout = QVBoxLayout(self)
420         main_layout.addWidget(label)
421         main_layout.addWidget(address_display)
422
423         self.setMouseTracking(True)
424         self.setWindowTitle("Electrum - " + _("Receive Bitcoin payment"))
425         self.setWindowFlags(Qt.Window|Qt.FramelessWindowHint|Qt.MSWindowsFixedSizeDialogHint)
426         self.layout().setSizeConstraint(QLayout.SetFixedSize)
427         #self.setFrameStyle(QFrame.WinPanel|QFrame.Raised)
428         #self.setAlignment(Qt.AlignCenter)
429
430     def popup(self):
431         parent = self.parent()
432         top_left_pos = parent.mapToGlobal(parent.rect().bottomLeft())
433         self.move(top_left_pos)
434         center_mouse_pos = self.mapToGlobal(self.rect().center())
435         QCursor.setPos(center_mouse_pos)
436         self.show()
437
438 class MiniActuator:
439
440     def __init__(self, wallet):
441         self.wallet = wallet
442
443     def copy_address(self, receive_popup):
444         addrs = [addr for addr in self.wallet.all_addresses()
445                  if not self.wallet.is_change(addr)]
446         copied_address = random.choice(addrs)
447         qApp.clipboard().setText(copied_address)
448         receive_popup.setup(copied_address)
449         receive_popup.popup()
450
451     def send(self, address, amount, parent_window):
452         dest_address = self.fetch_destination(address)
453
454         if dest_address is None or not self.wallet.is_valid(dest_address):
455             QMessageBox.warning(parent_window, _('Error'), 
456                 _('Invalid Bitcoin Address') + ':\n' + address, _('OK'))
457             return False
458
459         convert_amount = lambda amount: \
460             int(D(unicode(amount)) * bitcoin(1))
461         amount = convert_amount(amount)
462
463         if self.wallet.use_encryption:
464             password_dialog = PasswordDialog(parent_window)
465             password = password_dialog.run()
466             if not password:
467                 return
468         else:
469             password = None
470
471         fee = 0
472         # 0.1 BTC = 10000000
473         if amount < bitcoin(1) / 10:
474             # 0.01 BTC
475             fee = bitcoin(1) / 100
476
477         try:
478             tx = self.wallet.mktx(dest_address, amount, "", password, fee)
479         except BaseException as error:
480             QMessageBox.warning(parent_window, _('Error'), str(error), _('OK'))
481             return False
482             
483         status, message = self.wallet.sendtx(tx)
484         if not status:
485             QMessageBox.warning(parent_window, _('Error'), message, _('OK'))
486             return False
487
488         QMessageBox.information(parent_window, '',
489             _('Payment sent.') + '\n' + message, _('OK'))
490         return True
491
492     def fetch_destination(self, address):
493         recipient = unicode(address).strip()
494
495         # alias
496         match1 = re.match("^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$",
497                           recipient)
498
499         # label or alias, with address in brackets
500         match2 = re.match("(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>",
501                           recipient)
502         
503         if match1:
504             dest_address = \
505                 self.wallet.get_alias(recipient, True, 
506                                       self.show_message, self.question)
507             return dest_address
508         elif match2:
509             return match2.group(2)
510         else:
511             return recipient
512
513     def is_valid(self, address):
514         return self.wallet.is_valid(address)
515
516 class MiniDriver(QObject):
517
518     INITIALIZING = 0
519     CONNECTING = 1
520     SYNCHRONIZING = 2
521     READY = 3
522
523     def __init__(self, wallet, window):
524         super(QObject, self).__init__()
525
526         self.wallet = wallet
527         self.window = window
528
529         self.wallet.register_callback(self.update_callback)
530
531         self.state = None
532
533         self.initializing()
534         self.connect(self, SIGNAL("updatesignal()"), self.update)
535
536     # This is a hack to workaround that Qt does not like changing the
537     # window properties from this other thread before the runloop has
538     # been called from.
539     def update_callback(self):
540         self.emit(SIGNAL("updatesignal()"))
541
542     def update(self):
543         if not self.wallet.interface:
544             self.initializing()
545         elif not self.wallet.interface.is_connected:
546             self.connecting()
547         elif not self.wallet.blocks == -1:
548             self.connecting()
549         elif not self.wallet.is_up_to_date:
550             self.synchronizing()
551         else:
552             self.ready()
553
554         if self.wallet.up_to_date:
555             self.update_balance()
556             self.update_completions()
557
558     def initializing(self):
559         if self.state == self.INITIALIZING:
560             return
561         self.state = self.INITIALIZING
562         self.window.deactivate()
563
564     def connecting(self):
565         if self.state == self.CONNECTING:
566             return
567         self.state = self.CONNECTING
568         self.window.deactivate()
569
570     def synchronizing(self):
571         if self.state == self.SYNCHRONIZING:
572             return
573         self.state = self.SYNCHRONIZING
574         self.window.deactivate()
575
576     def ready(self):
577         if self.state == self.READY:
578             return
579         self.state = self.READY
580         self.window.activate()
581
582     def update_balance(self):
583         conf_balance, unconf_balance = self.wallet.get_balance()
584         balance = D(conf_balance + unconf_balance)
585         self.window.set_balances(balance)
586
587     def update_completions(self):
588         completions = []
589         for addr, label in self.wallet.labels.items():
590             if addr in self.wallet.addressbook:
591                 completions.append("%s <%s>" % (label, addr))
592         completions = completions + self.wallet.aliases.keys()
593         self.window.update_completions(completions)
594
595 if __name__ == "__main__":
596     app = QApplication(sys.argv)
597     with open(rsrc("style.css")) as style_file:
598         app.setStyleSheet(style_file.read())
599     mini = MiniWindow()
600     sys.exit(app.exec_())
601