Defer discovery of exchange rate until later to make program startup faster.
[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 i18n import _
5 import exchange_rate
6 import random
7 import re
8 import sys
9 import time
10 import wallet
11
12 try:
13     import lib.gui_qt as gui_qt
14 except ImportError:
15     import electrum.gui_qt as gui_qt
16
17 bitcoin = lambda v: v * 100000000
18
19 def IconButton(filename, parent=None):
20     pixmap = QPixmap(filename)
21     icon = QIcon(pixmap)
22     return QPushButton(icon, "", parent)
23
24 class Timer(QThread):
25     def run(self):
26         while True:
27             self.emit(SIGNAL('timersignal'))
28             time.sleep(0.5)
29
30 class ElectrumGui:
31
32     def __init__(self, wallet):
33         self.wallet = wallet
34         self.app = QApplication(sys.argv)
35         with open("data/style.css") as style_file:
36             self.app.setStyleSheet(style_file.read())
37
38     def main(self, url):
39         actuator = MiniActuator(self.wallet)
40         self.mini = MiniWindow(actuator, self.expand)
41         driver = MiniDriver(self.wallet, self.mini)
42
43         timer = Timer()
44         timer.start()
45         self.expert = gui_qt.ElectrumWindow(self.wallet)
46         self.expert.connect_slots(timer)
47         self.expert.update_wallet()
48
49         sys.exit(self.app.exec_())
50
51     def expand(self):
52         self.mini.hide()
53         self.expert.show()
54
55 class MiniWindow(QDialog):
56
57     def __init__(self, actuator, expand_callback):
58         super(MiniWindow, self).__init__()
59
60         self.actuator = actuator
61
62         accounts_button = IconButton("data/icons/accounts.png")
63         accounts_button.setObjectName("accounts_button")
64
65         self.accounts_selector = QMenu()
66         accounts_button.setMenu(self.accounts_selector)
67
68         interact_button = IconButton("data/icons/interact.png")
69         interact_button.setObjectName("interact_button")
70
71         app_menu = QMenu()
72         report_action = app_menu.addAction(_("&Report Bug"))
73         about_action = app_menu.addAction(_("&About Electrum"))
74         app_menu.addSeparator()
75         quit_action = app_menu.addAction(_("&Quit"))
76         interact_button.setMenu(app_menu)
77
78         self.connect(report_action, SIGNAL("triggered()"),
79                      self.show_report_bug)
80         self.connect(about_action, SIGNAL("triggered()"), self.show_about)
81         self.connect(quit_action, SIGNAL("triggered()"), self.close)
82
83         expand_button = IconButton("data/icons/expand.png")
84         expand_button.setObjectName("expand_button")
85         self.connect(expand_button, SIGNAL("clicked()"), expand_callback)
86
87         self.btc_balance = 0
88         self.quote_currencies = ("EUR", "USD", "GBP")
89         self.exchanger = exchange_rate.Exchanger(self.quote_currencies,
90                                                  self.refresh_balance)
91         QTimer.singleShot(1000, self.exchanger.discovery)
92
93         self.balance_label = BalanceLabel(self.change_quote_currency)
94         self.balance_label.setObjectName("balance_label")
95
96         copy_button = QPushButton(_("&Copy Address"))
97         copy_button.setObjectName("copy_button")
98         copy_button.setDefault(True)
99         self.connect(copy_button, SIGNAL("clicked()"),
100                      self.actuator.copy_address)
101
102         # Use QCompleter
103         self.address_input = TextedLineEdit(_("Enter a Bitcoin address..."))
104         self.address_input.setObjectName("address_input")
105         self.connect(self.address_input, SIGNAL("textChanged(QString)"),
106                      self.address_field_changed)
107         metrics = QFontMetrics(qApp.font())
108         self.address_input.setMinimumWidth(
109             metrics.width("1E4vM9q25xsyDwWwdqHUWnwshdWC9PykmL"))
110
111         self.valid_address = QCheckBox()
112         self.valid_address.setObjectName("valid_address")
113         self.valid_address.setEnabled(False)
114         self.valid_address.setChecked(False)
115
116         address_layout = QHBoxLayout()
117         address_layout.addWidget(self.address_input)
118         address_layout.addWidget(self.valid_address)
119
120         self.amount_input = TextedLineEdit(_("... and amount"))
121         self.amount_input.setObjectName("amount_input")
122         # This is changed according to the user's displayed balance
123         self.amount_validator = QDoubleValidator(self.amount_input)
124         self.amount_validator.setNotation(QDoubleValidator.StandardNotation)
125         self.amount_validator.setDecimals(8)
126         self.amount_input.setValidator(self.amount_validator)
127
128         amount_layout = QHBoxLayout()
129         amount_layout.addWidget(self.amount_input)
130         amount_layout.addStretch()
131
132         send_button = QPushButton(_("&Send"))
133         send_button.setObjectName("send_button")
134         self.connect(send_button, SIGNAL("clicked()"), self.send)
135
136         main_layout = QGridLayout(self)
137         main_layout.addWidget(accounts_button, 0, 0)
138         main_layout.addWidget(interact_button, 1, 0)
139         main_layout.addWidget(expand_button, 2, 0)
140
141         main_layout.addWidget(self.balance_label, 0, 1)
142         main_layout.addWidget(copy_button, 0, 2)
143
144         main_layout.addLayout(address_layout, 1, 1, 1, -1)
145
146         main_layout.addLayout(amount_layout, 2, 1)
147         main_layout.addWidget(send_button, 2, 2)
148
149         self.setWindowTitle("Electrum")
150         self.setWindowFlags(Qt.Window|Qt.MSWindowsFixedSizeDialogHint)
151         self.layout().setSizeConstraint(QLayout.SetFixedSize)
152         self.setObjectName("main_window")
153         self.show()
154
155     def closeEvent(self, event):
156         super(MiniWindow, self).closeEvent(event)
157         qApp.quit()
158
159     def activate(self):
160         pass
161
162     def deactivate(self):
163         pass
164
165     def change_quote_currency(self):
166         self.quote_currencies = \
167             self.quote_currencies[1:] + self.quote_currencies[0:1]
168         self.refresh_balance()
169
170     def refresh_balance(self):
171         self.set_balances(self.btc_balance)
172
173     def set_balances(self, btc_balance):
174         self.btc_balance = btc_balance
175         quote_currency = self.quote_currencies[0]
176         quote_balance = self.exchanger.exchange(btc_balance, quote_currency)
177         if quote_balance is None:
178             quote_text = ""
179         else:
180             quote_text = "(%.2f %s)" % ((quote_balance / bitcoin(1)),
181                                       quote_currency)
182         btc_balance = "%.2f" % (btc_balance / bitcoin(1))
183         self.balance_label.set_balances(btc_balance, quote_text)
184         main_account_info = \
185             "Checking - %s BTC %s" % (btc_balance, quote_text)
186         self.setWindowTitle("Electrum - %s" % main_account_info)
187         self.accounts_selector.clear()
188         self.accounts_selector.addAction("%s" % main_account_info)
189
190     def send(self):
191         if self.actuator.send(self.address_input.text(),
192                               self.amount_input.text(), self):
193             self.address_input.become_inactive()
194             self.amount_input.become_inactive()
195
196     def address_field_changed(self, address):
197         if self.actuator.is_valid(address):
198             self.valid_address.setChecked(True)
199         else:
200             self.valid_address.setChecked(False)
201
202     def show_about(self):
203         QMessageBox.about(self, "Electrum",
204             _("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."))
205
206     def show_report_bug(self):
207         QMessageBox.information(self, "Electrum - " + _("Reporting Bugs"),
208             _("Email bug reports to %s") % "genjix" + "@" + "riseup.net")
209
210 class BalanceLabel(QLabel):
211
212     def __init__(self, change_quote_currency, parent=None):
213         super(QLabel, self).__init__(_("Connecting..."), parent)
214         self.change_quote_currency = change_quote_currency
215
216     def set_balances(self, btc_balance, quote_text):
217         label_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)
218         self.setText(label_text)
219
220     def mousePressEvent(self, event):
221         self.change_quote_currency()
222
223 class TextedLineEdit(QLineEdit):
224
225     def __init__(self, inactive_text, parent=None):
226         super(QLineEdit, self).__init__(parent)
227         self.inactive_text = inactive_text
228         self.become_inactive()
229
230     def mousePressEvent(self, event):
231         if self.isReadOnly():
232             self.become_active()
233         QLineEdit.mousePressEvent(self, event)
234
235     def focusOutEvent(self, event):
236         if self.text() == "":
237             self.become_inactive()
238         QLineEdit.focusOutEvent(self, event)
239
240     def focusInEvent(self, event):
241         if self.isReadOnly():
242             self.become_active()
243         QLineEdit.focusInEvent(self, event)
244
245     def become_inactive(self):
246         self.setText(self.inactive_text)
247         self.setReadOnly(True)
248         self.recompute_style()
249
250     def become_active(self):
251         self.setText("")
252         self.setReadOnly(False)
253         self.recompute_style()
254
255     def recompute_style(self):
256         qApp.style().unpolish(self)
257         qApp.style().polish(self)
258         # also possible but more expensive:
259         #qApp.setStyleSheet(qApp.styleSheet())
260
261 def ok_cancel_buttons(dialog):
262     row_layout = QHBoxLayout()
263     row_layout.addStretch(1)
264     ok_button = QPushButton("OK")
265     row_layout.addWidget(ok_button)
266     ok_button.clicked.connect(dialog.accept)
267     cancel_button = QPushButton("Cancel")
268     row_layout.addWidget(cancel_button)
269     cancel_button.clicked.connect(dialog.reject)
270     return row_layout
271
272 class PasswordDialog(QDialog):
273
274     def __init__(self, parent):
275         super(QDialog, self).__init__(parent)
276
277         self.setModal(True)
278
279         self.password_input = QLineEdit()
280         self.password_input.setEchoMode(QLineEdit.Password)
281
282         main_layout = QVBoxLayout(self)
283         message = _('Please enter your password')
284         main_layout.addWidget(QLabel(message))
285
286         grid = QGridLayout()
287         grid.setSpacing(8)
288         grid.addWidget(QLabel(_('Password')), 1, 0)
289         grid.addWidget(self.password_input, 1, 1)
290         main_layout.addLayout(grid)
291
292         main_layout.addLayout(ok_cancel_buttons(self))
293         self.setLayout(main_layout) 
294
295     def run(self):
296         if not self.exec_():
297             return
298         return unicode(self.password_input.text())
299
300 class MiniActuator:
301
302     def __init__(self, wallet):
303         self.wallet = wallet
304
305     def copy_address(self):
306         addrs = [addr for addr in self.wallet.all_addresses()
307                  if not self.wallet.is_change(addr)]
308         qApp.clipboard().setText(random.choice(addrs))
309
310     def send(self, address, amount, parent_window):
311         dest_address = self.fetch_destination(address)
312
313         if dest_address is None or not self.wallet.is_valid(dest_address):
314             QMessageBox.warning(parent_window, _('Error'), 
315                 _('Invalid Bitcoin Address') + ':\n' + address, _('OK'))
316             return False
317
318         convert_amount = lambda amount: \
319             int(D(unicode(amount)) * bitcoin(1))
320         amount = convert_amount(amount)
321
322         if self.wallet.use_encryption:
323             password_dialog = PasswordDialog(parent_window)
324             password = password_dialog.run()
325             if not password:
326                 return
327         else:
328             password = None
329
330         fee = 0
331         # 0.1 BTC = 10000000
332         if amount < bitcoin(1) / 10:
333             # 0.01 BTC
334             fee = bitcoin(1) / 100
335
336         try:
337             tx = self.wallet.mktx(dest_address, amount, "", password, fee)
338         except BaseException as error:
339             QMessageBox.warning(parent_window, _('Error'), str(error), _('OK'))
340             return False
341             
342         status, message = self.wallet.sendtx(tx)
343         if not status:
344             QMessageBox.warning(parent_window, _('Error'), message, _('OK'))
345             return False
346
347         QMessageBox.information(parent_window, '',
348             _('Payment sent.') + '\n' + message, _('OK'))
349         return True
350
351     def fetch_destination(self, address):
352         recipient = unicode(address).strip()
353
354         # alias
355         match1 = re.match("^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$",
356                           recipient)
357
358         # label or alias, with address in brackets
359         match2 = re.match("(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>",
360                           recipient)
361         
362         if match1:
363             dest_address = \
364                 self.wallet.get_alias(recipient, True, 
365                                       self.show_message, self.question)
366             return dest_address
367         elif match2:
368             return match2.group(2)
369         else:
370             return recipient
371
372     def is_valid(self, address):
373         return self.wallet.is_valid(address)
374
375 class MiniDriver(QObject):
376
377     INITIALIZING = 0
378     CONNECTING = 1
379     SYNCHRONIZING = 2
380     READY = 3
381
382     def __init__(self, wallet, window):
383         super(QObject, self).__init__()
384
385         self.wallet = wallet
386         self.window = window
387
388         self.wallet.register_callback(self.update_callback)
389
390         self.state = None
391
392         self.initializing()
393         self.connect(self, SIGNAL("updatesignal()"), self.update)
394
395     # This is a hack to workaround that Qt does not like changing the
396     # window properties from this other thread before the runloop has
397     # been called from.
398     def update_callback(self):
399         self.emit(SIGNAL("updatesignal()"))
400
401     def update(self):
402         if not self.wallet.interface:
403             self.initializing()
404         elif not self.wallet.interface.is_connected:
405             self.connecting()
406         elif not self.wallet.blocks == -1:
407             self.connecting()
408         elif not self.wallet.is_up_to_date:
409             self.synchronizing()
410         else:
411             self.ready()
412
413         if self.wallet.up_to_date:
414             self.update_balance()
415
416     def initializing(self):
417         if self.state == self.INITIALIZING:
418             return
419         self.state = self.INITIALIZING
420         self.window.deactivate()
421
422     def connecting(self):
423         if self.state == self.CONNECTING:
424             return
425         self.state = self.CONNECTING
426         self.window.deactivate()
427
428     def synchronizing(self):
429         if self.state == self.SYNCHRONIZING:
430             return
431         self.state = self.SYNCHRONIZING
432         self.window.deactivate()
433
434     def ready(self):
435         if self.state == self.READY:
436             return
437         self.state = self.READY
438         self.window.activate()
439
440     def update_balance(self):
441         conf_balance, unconf_balance = self.wallet.get_balance()
442         balance = D(conf_balance + unconf_balance)
443         self.window.set_balances(balance)
444
445 if __name__ == "__main__":
446     app = QApplication(sys.argv)
447     with open("data/style.css") as style_file:
448         app.setStyleSheet(style_file.read())
449     mini = MiniWindow()
450     sys.exit(app.exec_())
451