a393f7d26f7c5bfa3ef3267f47018aeeb637d911
[electrum-nvc.git] / lib / gui_lite.py
1 from PyQt4.QtCore import *
2 from PyQt4.QtGui import *
3 from i18n import _
4 import decimal
5 import random
6 import re
7 import sys
8
9 def IconButton(filename, parent=None):
10     pixmap = QPixmap(filename)
11     icon = QIcon(pixmap)
12     return QPushButton(icon, "", parent)
13
14 class ElectrumGui:
15
16     def __init__(self, wallet):
17         self.wallet = wallet
18         self.app = QApplication(sys.argv)
19         with open("data/style.css") as style_file:
20             self.app.setStyleSheet(style_file.read())
21
22     def main(self, url):
23         actuator = MiniActuator(self.wallet)
24         mini = MiniWindow(actuator)
25         driver = MiniDriver(self.wallet, mini)
26         sys.exit(self.app.exec_())
27
28 class MiniWindow(QDialog):
29
30     def __init__(self, actuator):
31         super(MiniWindow, self).__init__()
32
33         self.actuator = actuator
34
35         accounts_button = IconButton("data/icons/accounts.png")
36         accounts_button.setObjectName("accounts_button")
37
38         self.accounts_selector = QMenu()
39         accounts_button.setMenu(self.accounts_selector)
40
41         interact_button = IconButton("data/icons/interact.png")
42         interact_button.setObjectName("interact_button")
43
44         app_menu = QMenu()
45         report_action = app_menu.addAction(_("&Report Bug"))
46         about_action = app_menu.addAction(_("&About Electrum"))
47         app_menu.addSeparator()
48         quit_action = app_menu.addAction(_("&Quit"))
49         interact_button.setMenu(app_menu)
50
51         self.connect(report_action, SIGNAL("triggered()"),
52                      self.show_report_bug)
53         self.connect(about_action, SIGNAL("triggered()"), self.show_about)
54         self.connect(quit_action, SIGNAL("triggered()"), self.close)
55
56         expand_button = IconButton("data/icons/expand.png")
57         expand_button.setObjectName("expand_button")
58
59         self.balance_label = BalanceLabel()
60         self.balance_label.setObjectName("balance_label")
61
62         copy_button = QPushButton(_("&Copy Address"))
63         copy_button.setObjectName("copy_button")
64         copy_button.setDefault(True)
65         self.connect(copy_button, SIGNAL("clicked()"),
66                      self.actuator.copy_address)
67
68         # Use QCompleter
69         self.address_input = TextedLineEdit(_("Enter a Bitcoin address..."))
70         self.address_input.setObjectName("address_input")
71         self.connect(self.address_input, SIGNAL("textChanged(QString)"),
72                      self.address_field_changed)
73         metrics = QFontMetrics(qApp.font())
74         self.address_input.setMinimumWidth(
75             metrics.width("1E4vM9q25xsyDwWwdqHUWnwshdWC9PykmL"))
76
77         self.valid_address = QCheckBox()
78         self.valid_address.setObjectName("valid_address")
79         self.valid_address.setEnabled(False)
80         self.valid_address.setChecked(False)
81
82         address_layout = QHBoxLayout()
83         address_layout.addWidget(self.address_input)
84         address_layout.addWidget(self.valid_address)
85
86         self.amount_input = TextedLineEdit(_("... and amount"))
87         self.amount_input.setObjectName("amount_input")
88         # This is changed according to the user's displayed balance
89         self.amount_validator = QDoubleValidator(self.amount_input)
90         self.amount_validator.setNotation(QDoubleValidator.StandardNotation)
91         self.amount_validator.setRange(0, 0)
92         self.amount_validator.setDecimals(2)
93         self.amount_input.setValidator(self.amount_validator)
94
95         amount_layout = QHBoxLayout()
96         amount_layout.addWidget(self.amount_input)
97         amount_layout.addStretch()
98
99         send_button = QPushButton(_("&Send"))
100         send_button.setObjectName("send_button")
101         self.connect(send_button, SIGNAL("clicked()"), self.send)
102
103         main_layout = QGridLayout(self)
104         main_layout.addWidget(accounts_button, 0, 0)
105         main_layout.addWidget(interact_button, 1, 0)
106         main_layout.addWidget(expand_button, 2, 0)
107
108         main_layout.addWidget(self.balance_label, 0, 1)
109         main_layout.addWidget(copy_button, 0, 2)
110
111         main_layout.addLayout(address_layout, 1, 1, 1, -1)
112
113         main_layout.addLayout(amount_layout, 2, 1)
114         main_layout.addWidget(send_button, 2, 2)
115
116         self.setWindowTitle("Electrum")
117         self.setWindowFlags(Qt.Window|Qt.MSWindowsFixedSizeDialogHint)
118         self.layout().setSizeConstraint(QLayout.SetFixedSize)
119         self.setObjectName("main_window")
120         self.show()
121
122     def closeEvent(self, event):
123         super(MiniWindow, self).closeEvent(event)
124         qApp.quit()
125
126     def activate(self):
127         pass
128
129     def deactivate(self):
130         pass
131
132     def set_balances(self, btc_balance, quote_balance, quote_currency):
133         self.balance_label.set_balances( \
134             btc_balance, quote_balance, quote_currency)
135         self.amount_validator.setRange(0, btc_balance)
136         main_account_info = \
137             "Checking - %s BTC (%s %s)" % (btc_balance,
138                                            quote_balance, quote_currency)
139         self.setWindowTitle("Electrum - %s" % main_account_info)
140         self.accounts_selector.clear()
141         self.accounts_selector.addAction("%s" % main_account_info)
142
143     def send(self):
144         self.actuator.send(self.address_input.text(),
145                            self.amount_input.text(), self)
146
147     def address_field_changed(self, address):
148         if self.actuator.is_valid(address):
149             self.valid_address.setChecked(True)
150         else:
151             self.valid_address.setChecked(False)
152
153     def show_about(self):
154         QMessageBox.about(self, "Electrum",
155             "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.")
156
157     def show_report_bug(self):
158         QMessageBox.information(self, "Electrum - Reporting Bugs",
159             "Email bug reports to %s@%s.net" % ("genjix", "riseup"))
160
161 class BalanceLabel(QLabel):
162
163     def __init__(self, parent=None):
164         super(QLabel, self).__init__("Connecting...", parent)
165
166     def set_balances(self, btc_balance, quote_balance, quote_currency):
167         label_text = "<span style='font-size: 16pt'>%s</span> <span style='font-size: 10pt'>BTC</span> <span style='font-size: 10pt'>(%s %s)</span>" % (btc_balance, quote_balance, quote_currency)
168         self.setText(label_text)
169
170 class TextedLineEdit(QLineEdit):
171
172     def __init__(self, inactive_text, parent=None):
173         super(QLineEdit, self).__init__(parent)
174         self.inactive_text = inactive_text
175         self.become_inactive()
176
177     def mousePressEvent(self, event):
178         if self.isReadOnly():
179             self.become_active()
180         QLineEdit.mousePressEvent(self, event)
181
182     def focusOutEvent(self, event):
183         if self.text() == "":
184             self.become_inactive()
185         QLineEdit.focusOutEvent(self, event)
186
187     def focusInEvent(self, event):
188         if self.isReadOnly():
189             self.become_active()
190         QLineEdit.focusInEvent(self, event)
191
192     def become_inactive(self):
193         self.setText(self.inactive_text)
194         self.setReadOnly(True)
195         self.recompute_style()
196
197     def become_active(self):
198         self.setText("")
199         self.setReadOnly(False)
200         self.recompute_style()
201
202     def recompute_style(self):
203         qApp.style().unpolish(self)
204         qApp.style().polish(self)
205         # also possible but more expensive:
206         #qApp.setStyleSheet(qApp.styleSheet())
207
208 class MiniActuator:
209
210     def __init__(self, wallet):
211         self.wallet = wallet
212
213     def copy_address(self):
214         addrs = [addr for addr in self.wallet.all_addresses()
215                  if not self.wallet.is_change(addr)]
216         qApp.clipboard().setText(random.choice(addrs))
217
218     def send(self, address, amount, parent_window):
219         recipient = unicode(address).strip()
220
221         # alias
222         match1 = re.match(ALIAS_REGEXP, r)
223         # label or alias, with address in brackets
224         match2 = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
225         
226         if match1:
227             dest_address = \
228                 self.wallet.get_alias(recipient, True, 
229                                       self.show_message, self.question)
230             if not dest_address:
231                 return
232         elif match2:
233             dest_address = match2.group(2)
234         else:
235             dest_address = recipient
236
237         if not self.wallet.is_valid(dest_address):
238             QMessageBox.warning(parent_window, _('Error'), 
239                 _('Invalid Bitcoin Address') + ':\n' + dest_address, _('OK'))
240             return
241
242         convert_amount = lambda amount: \
243             int(decimal.Decimal(unicode(amount)) * 100000000)
244
245         amount = convert_amount(amount)
246
247         if self.wallet.use_encryption:
248             password = self.password_dialog()
249             if not password:
250                 return
251         else:
252             password = None
253
254         try:
255             tx = self.wallet.mktx(dest_address, amount, "", password, fee)
256         except BaseException, e:
257             self.show_message(str(e))
258             return
259             
260         status, msg = self.wallet.sendtx( tx )
261         if status:
262             QMessageBox.information(self, '', _('Payment sent.')+'\n'+msg, _('OK'))
263             self.do_clear()
264             self.update_contacts_tab()
265         else:
266             QMessageBox.warning(self, _('Error'), msg, _('OK'))
267
268     def is_valid(self, address):
269         return self.wallet.is_valid(address)
270
271 class MiniDriver(QObject):
272
273     INITIALIZING = 0
274     CONNECTING = 1
275     SYNCHRONIZING = 2
276     READY = 3
277
278     def __init__(self, wallet, window):
279         super(QObject, self).__init__()
280
281         self.wallet = wallet
282         self.window = window
283
284         self.wallet.gui_callback = self.update_callback
285
286         self.state = None
287
288         self.initializing()
289         self.connect(self, SIGNAL("updatesignal()"), self.update)
290
291     # This is a hack to workaround that Qt does not like changing the
292     # window properties from this other thread before the runloop has
293     # been called from.
294     def update_callback(self):
295         self.emit(SIGNAL("updatesignal()"))
296
297     def update(self):
298         if not self.wallet.interface:
299             self.initializing()
300         elif not self.wallet.interface.is_connected:
301             self.connecting()
302         elif not self.wallet.blocks == -1:
303             self.connecting()
304         elif not self.wallet.is_up_to_date:
305             self.synchronizing()
306         else:
307             self.ready()
308
309         if self.wallet.up_to_date:
310             self.update_balance()
311
312     def initializing(self):
313         if self.state == self.INITIALIZING:
314             return
315         self.state = self.INITIALIZING
316         self.window.deactivate()
317
318     def connecting(self):
319         if self.state == self.CONNECTING:
320             return
321         self.state = self.CONNECTING
322         self.window.deactivate()
323
324     def synchronizing(self):
325         if self.state == self.SYNCHRONIZING:
326             return
327         self.state = self.SYNCHRONIZING
328         self.window.deactivate()
329
330     def ready(self):
331         if self.state == self.READY:
332             return
333         self.state = self.READY
334         self.window.activate()
335
336     def update_balance(self):
337         conf_balance, unconf_balance = self.wallet.get_balance()
338         balance = conf_balance if unconf_balance is None else unconf_balance
339         self.window.set_balances(balance, balance * 6, 'EUR')
340
341 if __name__ == "__main__":
342     app = QApplication(sys.argv)
343     with open("data/style.css") as style_file:
344         app.setStyleSheet(style_file.read())
345     mini = MiniWindow()
346     sys.exit(app.exec_())
347