revert previous commit
[electrum-nvc.git] / lib / gui_qt.py
1 #!/usr/bin/env python
2 #
3 # Electrum - lightweight Bitcoin client
4 # Copyright (C) 2012 thomasv@gitorious
5 #
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
19 import sys, time, datetime, re
20
21 try:
22     import PyQt4
23 except:
24     print "could not import PyQt4"
25     print "on Linux systems, you may try 'sudo apt-get install python-qt4'"
26     sys.exit(1)
27
28
29 from PyQt4.QtGui import *
30 from PyQt4.QtCore import *
31 import PyQt4.QtCore as QtCore
32 import PyQt4.QtGui as QtGui
33 from interface import DEFAULT_SERVERS
34
35 try:
36     import icons_rc
37 except:
38     print "Could not import icons_rp.py"
39     print "Please generate it with: 'pyrcc4 icons.qrc -o icons_rc.py'"
40     sys.exit(1)
41
42 from wallet import format_satoshis
43 from decimal import Decimal
44
45 import platform
46 MONOSPACE_FONT = 'Lucida Console' if platform.system() == 'Windows' else 'monospace'
47     
48
49 def numbify(entry, is_int = False):
50     text = unicode(entry.text()).strip()
51     chars = '0123456789'
52     if not is_int: chars +='.'
53     s = ''.join([i for i in text if i in chars])
54     if not is_int:
55         if '.' in s:
56             p = s.find('.')
57             s = s.replace('.','')
58             s = s[:p] + '.' + s[p:p+8]
59         try:
60             amount = int( Decimal(s) * 100000000 )
61         except:
62             amount = None
63     else:
64         try:
65             amount = int( s )
66         except:
67             amount = None
68     entry.setText(s)
69     return amount
70
71
72 class Timer(QtCore.QThread):
73     def run(self):
74         while True:
75             self.emit(QtCore.SIGNAL('timersignal'))
76             time.sleep(0.5)
77
78 class EnterButton(QPushButton):
79     def __init__(self, text, func):
80         QPushButton.__init__(self, text)
81         self.func = func
82         self.clicked.connect(func)
83
84     def keyPressEvent(self, e):
85         if e.key() == QtCore.Qt.Key_Return:
86             apply(self.func,())
87
88 class StatusBarButton(QPushButton):
89     def __init__(self, icon, tooltip, func):
90         QPushButton.__init__(self, icon, '')
91         self.setToolTip(tooltip)
92         self.setFlat(True)
93         self.setMaximumWidth(25)
94         self.clicked.connect(func)
95         self.func = func
96
97     def keyPressEvent(self, e):
98         if e.key() == QtCore.Qt.Key_Return:
99             apply(self.func,())
100
101
102 class QRCodeWidget(QWidget):
103
104     def __init__(self, addr):
105         super(QRCodeWidget, self).__init__()
106         self.setGeometry(300, 300, 350, 350)
107         self.set_addr(addr)
108
109     def set_addr(self, addr):
110         from electrum import pyqrnative
111         self.addr = addr
112         self.qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.L)
113         self.qr.addData(addr)
114         self.qr.make()
115         
116     def paintEvent(self, e):
117         qp = QtGui.QPainter()
118         qp.begin(self)
119         boxsize = 7
120         size = self.qr.getModuleCount()*boxsize
121         k = self.qr.getModuleCount()
122         black = QColor(0, 0, 0, 255)
123         white = QColor(255, 255, 255, 255)
124         for r in range(k):
125             for c in range(k):
126                 if self.qr.isDark(r, c):
127                     qp.setBrush(black)
128                     qp.setPen(black)
129                 else:
130                     qp.setBrush(white)
131                     qp.setPen(white)
132                 qp.drawRect(c*boxsize, r*boxsize, boxsize, boxsize)
133         qp.end()
134         
135
136
137 def ok_cancel_buttons(dialog):
138     hbox = QHBoxLayout()
139     hbox.addStretch(1)
140     b = QPushButton("OK")
141     hbox.addWidget(b)
142     b.clicked.connect(dialog.accept)
143     b = QPushButton("Cancel")
144     hbox.addWidget(b)
145     b.clicked.connect(dialog.reject)
146     return hbox
147
148
149 class ElectrumWindow(QMainWindow):
150
151     def __init__(self, wallet):
152         QMainWindow.__init__(self)
153         self.wallet = wallet
154         self.wallet.gui_callback = self.update_callback
155
156         self.funds_error = False
157
158         self.tabs = tabs = QTabWidget(self)
159         tabs.addTab(self.create_history_tab(), 'History')
160         if self.wallet.seed:
161             tabs.addTab(self.create_send_tab(),    'Send')
162         tabs.addTab(self.create_receive_tab(), 'Receive')
163         tabs.addTab(self.create_contacts_tab(),'Contacts')
164         tabs.addTab(self.create_wall_tab(),    'Wall')
165         tabs.setMinimumSize(600, 400)
166         tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
167         self.setCentralWidget(tabs)
168         self.create_status_bar()
169         self.setGeometry(100,100,840,400)
170         title = 'Electrum ' + self.wallet.electrum_version + '  -  ' + self.wallet.path
171         if not self.wallet.seed: title += ' [seedless]'
172         self.setWindowTitle( title )
173         self.show()
174
175         QShortcut(QKeySequence("Ctrl+W"), self, self.close)
176         QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
177         QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
178         QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
179         
180         self.connect(self, QtCore.SIGNAL('updatesignal'), self.update_wallet)
181
182
183     def connect_slots(self, sender):
184         if self.wallet.seed:
185             self.connect(sender, QtCore.SIGNAL('timersignal'), self.check_recipient)
186             self.previous_payto_e=''
187
188     def check_recipient(self):
189         if self.payto_e.hasFocus():
190             return
191         r = unicode( self.payto_e.text() )
192         if r != self.previous_payto_e:
193             self.previous_payto_e = r
194             r = r.strip()
195             if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r):
196                 try:
197                     to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
198                 except:
199                     return
200                 if to_address:
201                     s = r + ' <' + to_address + '>'
202                     self.payto_e.setText(s)
203
204
205     def update_callback(self):
206         self.emit(QtCore.SIGNAL('updatesignal'))
207
208     def update_wallet(self):
209         if self.wallet.interface.is_connected:
210             if self.wallet.blocks == -1:
211                 text = "Connecting..."
212                 icon = QIcon(":icons/status_disconnected.png")
213             elif self.wallet.blocks == 0:
214                 text = "Server not ready"
215                 icon = QIcon(":icons/status_disconnected.png")
216             elif not self.wallet.up_to_date:
217                 text = "Synchronizing..."
218                 icon = QIcon(":icons/status_waiting.png")
219             else:
220                 c, u = self.wallet.get_balance()
221                 text =  "Balance: %s "%( format_satoshis(c,False,self.wallet.num_zeros) )
222                 if u: text +=  "[%s unconfirmed]"%( format_satoshis(u,True,self.wallet.num_zeros).strip() )
223                 icon = QIcon(":icons/status_connected.png")
224         else:
225             text = "Not connected"
226             icon = QIcon(":icons/status_disconnected.png")
227
228         if self.funds_error:
229             text = "Not enough funds"
230
231         self.statusBar().showMessage(text)
232         self.status_button.setIcon( icon )
233
234         if self.wallet.up_to_date:
235             self.textbox.setText( self.wallet.banner )
236             self.update_history_tab()
237             self.update_receive_tab()
238             self.update_contacts_tab()
239
240
241     def create_history_tab(self):
242         self.history_list = w = QTreeWidget(self)
243         #print w.getContentsMargins()
244         w.setColumnCount(5)
245         w.setColumnWidth(0, 40) 
246         w.setColumnWidth(1, 140) 
247         w.setColumnWidth(2, 350) 
248         w.setColumnWidth(3, 140) 
249         w.setColumnWidth(4, 140) 
250         w.setHeaderLabels( [ '', 'Date', 'Description', 'Amount', 'Balance'] )
251         self.connect(w, SIGNAL('itemActivated(QTreeWidgetItem*, int)'), self.tx_details)
252         self.connect(w, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
253         self.connect(w, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
254         return w
255
256     def tx_details(self, item, column):
257         tx_hash = str(item.toolTip(0))
258         tx = self.wallet.tx_history.get(tx_hash)
259
260         if tx['height']:
261             conf = self.wallet.blocks - tx['height'] + 1
262             time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3]
263         else:
264             conf = 0
265             time_str = 'pending'
266
267         tx_details = "Transaction Details:\n\n" \
268             + "Transaction ID:\n" + tx_hash + "\n\n" \
269             + "Status: %d confirmations\n\n"%conf  \
270             + "Date: %s\n\n"%time_str \
271             + "Inputs:\n-"+ '\n-'.join(tx['inputs']) + "\n\n" \
272             + "Outputs:\n-"+ '\n-'.join(tx['outputs'])
273
274         r = self.wallet.receipts.get(tx_hash)
275         if r:
276             tx_details += "\n_______________________________________" \
277                 + '\n\nSigned URI: ' + r[2] \
278                 + "\n\nSigned by: " + r[0] \
279                 + '\n\nSignature: ' + r[1]
280
281         QMessageBox.information(self, 'Details', tx_details, 'OK')
282
283
284     def tx_label_clicked(self, item, column):
285         if column==2 and item.isSelected():
286             tx_hash = str(item.toolTip(0))
287             self.is_edit=True
288             #if not self.wallet.labels.get(tx_hash): item.setText(2,'')
289             item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
290             self.history_list.editItem( item, column )
291             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
292             self.is_edit=False
293
294     def tx_label_changed(self, item, column):
295         if self.is_edit: 
296             return
297         self.is_edit=True
298         tx_hash = str(item.toolTip(0))
299         tx = self.wallet.tx_history.get(tx_hash)
300         s = self.wallet.labels.get(tx_hash)
301         text = unicode( item.text(2) )
302         if text: 
303             self.wallet.labels[tx_hash] = text
304             item.setForeground(2, QBrush(QColor('black')))
305         else:
306             if s: self.wallet.labels.pop(tx_hash)
307             text = tx['default_label']
308             item.setText(2, text)
309             item.setForeground(2, QBrush(QColor('gray')))
310         self.is_edit=False
311
312     def address_label_clicked(self, item, column, l):
313         if column==1 and item.isSelected():
314             addr = unicode( item.text(0) )
315             if addr in map(lambda x:x[1], self.wallet.aliases.values()):
316                 return
317             item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
318             l.editItem( item, column )
319             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
320
321     def address_label_changed(self, item, column, l):
322         addr = unicode( item.text(0) )
323         text = unicode( item.text(1) )
324         if text:
325             self.wallet.labels[addr] = text
326         else:
327             s = self.wallet.labels.get(addr)
328             if s: self.wallet.labels.pop(addr)
329         self.update_history_tab()
330
331     def update_history_tab(self):
332         self.history_list.clear()
333         balance = 0
334         for tx in self.wallet.get_tx_history():
335             tx_hash = tx['tx_hash']
336             if tx['height']:
337                 conf = self.wallet.blocks - tx['height'] + 1
338                 time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3]
339                 icon = QIcon(":icons/confirmed.png")
340             else:
341                 conf = 0
342                 time_str = 'pending'
343                 icon = QIcon(":icons/unconfirmed.png")
344             v = tx['value']
345             balance += v 
346             label = self.wallet.labels.get(tx_hash)
347             is_default_label = (label == '') or (label is None)
348             if is_default_label: label = tx['default_label']
349
350             item = QTreeWidgetItem( [ '', time_str, label, format_satoshis(v,True,self.wallet.num_zeros), format_satoshis(balance,False,self.wallet.num_zeros)] )
351             item.setFont(2, QFont(MONOSPACE_FONT))
352             item.setFont(3, QFont(MONOSPACE_FONT))
353             item.setFont(4, QFont(MONOSPACE_FONT))
354             item.setToolTip(0, tx_hash)
355             if is_default_label:
356                 item.setForeground(2, QBrush(QColor('grey')))
357
358             item.setIcon(0, icon)
359             self.history_list.insertTopLevelItem(0,item)
360
361
362     def create_send_tab(self):
363         w = QWidget()
364
365         grid = QGridLayout()
366         grid.setSpacing(8)
367         grid.setColumnMinimumWidth(3,300)
368         grid.setColumnStretch(4,1)
369
370         self.payto_e = QLineEdit()
371         grid.addWidget(QLabel('Pay to'), 1, 0)
372         grid.addWidget(self.payto_e, 1, 1, 1, 3)
373
374         self.message_e = QLineEdit()
375         grid.addWidget(QLabel('Description'), 2, 0)
376         grid.addWidget(self.message_e, 2, 1, 1, 3)
377
378         self.amount_e = QLineEdit()
379         grid.addWidget(QLabel('Amount'), 3, 0)
380         grid.addWidget(self.amount_e, 3, 1, 1, 2)
381         
382         self.fee_e = QLineEdit()
383         grid.addWidget(QLabel('Fee'), 4, 0)
384         grid.addWidget(self.fee_e, 4, 1, 1, 2)
385         
386         b = EnterButton("Send", self.do_send)
387         grid.addWidget(b, 5, 1)
388
389         b = EnterButton("Clear",self.do_clear)
390         grid.addWidget(b, 5, 2)
391
392         self.payto_sig = QLabel('')
393         grid.addWidget(self.payto_sig, 6, 0, 1, 4)
394
395         w.setLayout(grid) 
396         w.show()
397
398         w2 = QWidget()
399         vbox = QVBoxLayout()
400         vbox.addWidget(w)
401         vbox.addStretch(1)
402         w2.setLayout(vbox)
403
404         def entry_changed( is_fee ):
405             self.funds_error = False
406             amount = numbify(self.amount_e)
407             fee = numbify(self.fee_e)
408             if not is_fee: fee = None
409             if amount is None:
410                 return
411             inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
412             if not is_fee:
413                 self.fee_e.setText( str( Decimal( fee ) / 100000000 ) )
414             if inputs:
415                 palette = QPalette()
416                 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
417             else:
418                 palette = QPalette()
419                 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
420                 self.funds_error = True
421             self.amount_e.setPalette(palette)
422             self.fee_e.setPalette(palette)
423
424         self.amount_e.textChanged.connect(lambda: entry_changed(False) )
425         self.fee_e.textChanged.connect(lambda: entry_changed(True) )
426
427         return w2
428
429     def do_send(self):
430
431         label = unicode( self.message_e.text() )
432         r = unicode( self.payto_e.text() )
433         r = r.strip()
434
435         m1 = re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r)
436         m2 = re.match('(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+) \<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
437         
438         if m1:
439             to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
440             if not to_address:
441                 return
442         elif m2:
443             to_address = m2.group(5)
444         else:
445             to_address = r
446
447         if not self.wallet.is_valid(to_address):
448             QMessageBox.warning(self, 'Error', 'Invalid Bitcoin Address:\n'+to_address, 'OK')
449             return
450
451         try:
452             amount = int( Decimal( unicode( self.amount_e.text())) * 100000000 )
453         except:
454             QMessageBox.warning(self, 'Error', 'Invalid Amount', 'OK')
455             return
456         try:
457             fee = int( Decimal( unicode( self.fee_e.text())) * 100000000 )
458         except:
459             QMessageBox.warning(self, 'Error', 'Invalid Fee', 'OK')
460             return
461
462         if self.wallet.use_encryption:
463             password = self.password_dialog()
464             if not password:
465                 return
466         else:
467             password = None
468
469         try:
470             tx = self.wallet.mktx( to_address, amount, label, password, fee )
471         except BaseException, e:
472             self.show_message(e.message)
473             return
474             
475         status, msg = self.wallet.sendtx( tx )
476         if status:
477             QMessageBox.information(self, '', 'Payment sent.\n'+msg, 'OK')
478             self.do_clear()
479             self.update_contacts_tab()
480         else:
481             QMessageBox.warning(self, 'Error', msg, 'OK')
482
483
484     def set_url(self, url):
485         payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question)
486         self.tabs.setCurrentIndex(1)
487         self.payto_e.setText(payto)
488         self.message_e.setText(message)
489         self.amount_e.setText(amount)
490         if identity:
491             self.set_frozen(self.payto_e,True)
492             self.set_frozen(self.amount_e,True)
493             self.set_frozen(self.message_e,True)
494             self.payto_sig.setText( '      The bitcoin URI was signed by ' + identity )
495         else:
496             self.payto_sig.setVisible(False)
497
498     def do_clear(self):
499         self.payto_sig.setVisible(False)
500         for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
501             e.setText('')
502             self.set_frozen(e,False)
503
504     def set_frozen(self,entry,frozen):
505         if frozen:
506             entry.setReadOnly(True)
507             entry.setFrame(False)
508             palette = QPalette()
509             palette.setColor(entry.backgroundRole(), QColor('lightgray'))
510             entry.setPalette(palette)
511         else:
512             entry.setReadOnly(False)
513             entry.setFrame(True)
514             palette = QPalette()
515             palette.setColor(entry.backgroundRole(), QColor('white'))
516             entry.setPalette(palette)
517
518
519
520
521     def clear_buttons(self, hbox):
522         while hbox.count(): hbox.removeItem(hbox.itemAt(0))
523
524     def add_buttons(self, l, hbox, is_recv):
525         self.clear_buttons(hbox)
526
527         i = l.currentItem()
528         if not i: return
529         addr = unicode( i.text(0) )
530
531         hbox.addWidget(EnterButton("QR",lambda: self.show_address_qrcode(addr)))
532         hbox.addWidget(EnterButton("Copy to Clipboard", lambda: self.app.clipboard().setText(addr)))
533         if is_recv:
534             def toggle_freeze(addr):
535                 if addr in self.wallet.frozen_addresses:
536                     self.wallet.frozen_addresses.remove(addr)
537                 else:
538                     self.wallet.frozen_addresses.append(addr)
539                 self.wallet.save()
540                 self.update_receive_tab()
541
542             t = "Unfreeze" if addr in self.wallet.frozen_addresses else "Freeze"
543             hbox.addWidget(EnterButton(t, lambda: toggle_freeze(addr)))
544
545         else:
546             def payto(addr):
547                 if not addr:return
548                 self.tabs.setCurrentIndex(1)
549                 self.payto_e.setText(addr)
550                 self.amount_e.setFocus()
551             hbox.addWidget(EnterButton('Pay to', lambda: payto(addr)))
552             hbox.addWidget(EnterButton("New", self.newaddress_dialog))
553         hbox.addStretch(1)
554
555
556     def create_receive_tab(self):
557         l = QTreeWidget(self)
558         l.setColumnCount(4)
559         l.setColumnWidth(0, 350) 
560         l.setColumnWidth(1, 330)
561         l.setColumnWidth(2, 100) 
562         l.setColumnWidth(3, 10) 
563         l.setHeaderLabels( ['Address', 'Label','Balance','Tx'])
564
565         w = QWidget()
566         vbox = QVBoxLayout()
567         w.setLayout(vbox)
568
569         vbox.setMargin(0)
570         vbox.setSpacing(0)
571         vbox.addWidget(l)
572         buttons = QWidget()
573         vbox.addWidget(buttons)
574
575         hbox = QHBoxLayout()
576         hbox.setMargin(0)
577         hbox.setSpacing(0)
578         buttons.setLayout(hbox)
579
580         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l))
581         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l))
582         self.connect(l, SIGNAL('itemClicked(QTreeWidgetItem*, int)'), lambda: self.add_buttons(l, hbox, True))
583         self.receive_list = l
584         self.receive_buttons_hbox = hbox
585         return w
586
587     def create_contacts_tab(self):
588         l = QTreeWidget(self)
589         l.setColumnCount(3)
590         l.setColumnWidth(0, 350) 
591         l.setColumnWidth(1, 330)
592         l.setColumnWidth(2, 20) 
593         l.setHeaderLabels( ['Address', 'Label','Tx'])
594
595         w = QWidget()
596         vbox = QVBoxLayout()
597         w.setLayout(vbox)
598
599         vbox.setMargin(0)
600         vbox.setSpacing(0)
601         vbox.addWidget(l)
602         buttons = QWidget()
603         vbox.addWidget(buttons)
604
605         hbox = QHBoxLayout()
606         hbox.setMargin(0)
607         hbox.setSpacing(0)
608         buttons.setLayout(hbox)
609
610         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l))
611         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l))
612         self.connect(l, SIGNAL('itemActivated(QTreeWidgetItem*, int)'), self.show_contact_details)
613         self.connect(l, SIGNAL('itemClicked(QTreeWidgetItem*, int)'), lambda: self.add_buttons(l, hbox, False))
614
615         self.contacts_list = l
616         self.contacts_buttons_hbox = hbox
617         return w
618
619     def update_receive_tab(self):
620         self.receive_list.clear()
621         self.clear_buttons(self.receive_buttons_hbox)
622
623         for address in self.wallet.all_addresses():
624             if self.wallet.is_change(address):continue
625             label = self.wallet.labels.get(address,'')
626             n = 0 
627             h = self.wallet.history.get(address,[])
628             for item in h:
629                 if not item['is_input'] : n=n+1
630             tx = "None" if n==0 else "%d"%n
631
632             c, u = self.wallet.get_addr_balance(address)
633             balance = format_satoshis( c + u, False, self.wallet.num_zeros )
634             if address in self.wallet.frozen_addresses: 
635                 balance += '[F]'
636
637             item = QTreeWidgetItem( [ address, label, balance, tx] )
638             item.setFont(0, QFont(MONOSPACE_FONT))
639             self.receive_list.addTopLevelItem(item)
640
641     def show_contact_details(self, item, column):
642         m = unicode(item.text(0))
643         a = self.wallet.aliases.get(m)
644         if a:
645             if a[0] in self.wallet.authorities.keys():
646                 s = self.wallet.authorities.get(a[0])
647             else:
648                 s = "self-signed"
649             msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0]
650             QMessageBox.information(self, 'Alias', msg, 'OK')
651
652     def update_contacts_tab(self):
653         self.contacts_list.clear()
654         self.clear_buttons(self.contacts_buttons_hbox)
655
656         for alias, v in self.wallet.aliases.items():
657             s, target = v
658             item = QTreeWidgetItem( [ target, alias, '-'] )
659             self.contacts_list.addTopLevelItem(item)
660             
661         for address in self.wallet.addressbook:
662             label = self.wallet.labels.get(address,'')
663             n = 0 
664             for item in self.wallet.tx_history.values():
665                 if address in item['outputs'] : n=n+1
666             tx = "None" if n==0 else "%d"%n
667             item = QTreeWidgetItem( [ address, label, tx] )
668             item.setFont(0, QFont(MONOSPACE_FONT))
669             self.contacts_list.addTopLevelItem(item)
670
671
672     def create_wall_tab(self):
673         self.textbox = textbox = QTextEdit(self)
674         textbox.setFont(QFont(MONOSPACE_FONT))
675         textbox.setReadOnly(True)
676         return textbox
677
678     def create_status_bar(self):
679         sb = QStatusBar()
680         sb.setFixedHeight(35)
681         if self.wallet.seed:
682             sb.addPermanentWidget( StatusBarButton( QIcon(":icons/lock.png"), "Password", lambda: self.change_password_dialog(self.wallet, self) ) )
683         sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), "Preferences", self.settings_dialog ) )
684         if self.wallet.seed:
685             sb.addPermanentWidget( StatusBarButton( QIcon(":icons/seed.png"), "Seed", lambda: self.show_seed_dialog(self.wallet, self) ) )
686         self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), "Network", lambda: self.network_dialog(self.wallet, self) ) 
687         sb.addPermanentWidget( self.status_button )
688         self.setStatusBar(sb)
689
690     def newaddress_dialog(self):
691         text, ok = QInputDialog.getText(self, 'New Contact', 'Address:')
692         address = unicode(text)
693         if ok:
694             if self.wallet.is_valid(address):
695                 self.wallet.addressbook.append(address)
696                 self.wallet.save()
697                 self.update_contacts_tab()
698             else:
699                 QMessageBox.warning(self, 'Error', 'Invalid Address', 'OK')
700
701     @staticmethod
702     def show_seed_dialog(wallet, parent=None):
703         from electrum import mnemonic
704
705         if not wallet.seed:
706             QMessageBox.information(parent, 'Message', 'No seed', 'OK')
707             return
708
709         if wallet.use_encryption:
710             password = parent.password_dialog()
711             if not password: return
712         else:
713             password = None
714             
715         try:
716             seed = wallet.pw_decode( wallet.seed, password)
717         except:
718             QMessageBox.warning(parent, 'Error', 'Invalid Password', 'OK')
719             return
720
721         msg = "Your wallet generation seed is:\n\n" + seed \
722               + "\n\nPlease keep it in a safe place; if you lose it,\nyou will not be able to restore your wallet.\n\n" \
723               + "Equivalently, your wallet seed can be stored and\nrecovered with the following mnemonic code:\n\n\"" \
724               + ' '.join(mnemonic.mn_encode(seed)) + "\"\n\n\n"
725
726         d = QDialog(None)
727         d.setModal(1)
728         d.setWindowTitle("Seed")
729         d.setMinimumSize(400, 270)
730
731         vbox = QVBoxLayout()
732         hbox = QHBoxLayout()
733         vbox2 = QVBoxLayout()
734         l = QLabel()
735         l.setPixmap(QPixmap(":icons/seed.png").scaledToWidth(56))
736         vbox2.addWidget(l)
737         vbox2.addStretch(1)
738         hbox.addLayout(vbox2)
739         hbox.addWidget(QLabel(msg))
740         vbox.addLayout(hbox)
741
742         hbox = QHBoxLayout()
743         hbox.addStretch(1)
744
745
746         if parent:
747             app = parent.app
748         else:
749             app = QApplication
750
751         b = QPushButton("Copy to Clipboard")
752         b.clicked.connect(lambda: app.clipboard().setText(' '.join(mnemonic.mn_encode(seed))))
753         hbox.addWidget(b)
754         b = QPushButton("View as QR Code")
755         b.clicked.connect(lambda: ElectrumWindow.show_seed_qrcode(seed))
756         hbox.addWidget(b)
757
758         b = QPushButton("OK")
759         b.clicked.connect(d.accept)
760         hbox.addWidget(b)
761         vbox.addLayout(hbox)
762         d.setLayout(vbox)
763         d.exec_()
764
765     @staticmethod
766     def show_seed_qrcode(seed):
767         if not seed: return
768         d = QDialog(None)
769         d.setModal(1)
770         d.setWindowTitle("Seed")
771         d.setMinimumSize(270, 300)
772         vbox = QVBoxLayout()
773         vbox.addWidget(QRCodeWidget(seed))
774         hbox = QHBoxLayout()
775         hbox.addStretch(1)
776         b = QPushButton("OK")
777         hbox.addWidget(b)
778         b.clicked.connect(d.accept)
779
780         vbox.addLayout(hbox)
781         d.setLayout(vbox)
782         d.exec_()
783
784     def show_address_qrcode(self,address):
785         if not address: return
786         d = QDialog(None)
787         d.setModal(1)
788         d.setWindowTitle(address)
789         d.setMinimumSize(270, 350)
790         vbox = QVBoxLayout()
791         qrw = QRCodeWidget(address)
792         vbox.addWidget(qrw)
793
794         hbox = QHBoxLayout()
795         amount_e = QLineEdit()
796         hbox.addWidget(QLabel('Amount'))
797         hbox.addWidget(amount_e)
798         vbox.addLayout(hbox)
799
800         #hbox = QHBoxLayout()
801         #label_e = QLineEdit()
802         #hbox.addWidget(QLabel('Label'))
803         #hbox.addWidget(label_e)
804         #vbox.addLayout(hbox)
805
806         def amount_changed():
807             amount = numbify(amount_e)
808             #label = str( label_e.getText() )
809             if amount is not None:
810                 qrw.set_addr('bitcoin:%s?amount=%s'%(address,str( Decimal(amount) /100000000)))
811             else:
812                 qrw.set_addr( address )
813             qrw.repaint()
814
815         def do_save():
816             from electrum import bmp
817             bmp.save_qrcode(qrw.qr, "qrcode.bmp")
818             self.show_message("QR code saved to file 'qrcode.bmp'")
819             
820         amount_e.textChanged.connect( amount_changed )
821
822         hbox = QHBoxLayout()
823         hbox.addStretch(1)
824         b = QPushButton("Save")
825         b.clicked.connect(do_save)
826         hbox.addWidget(b)
827         b = QPushButton("Close")
828         hbox.addWidget(b)
829         b.clicked.connect(d.accept)
830
831         vbox.addLayout(hbox)
832         d.setLayout(vbox)
833         d.exec_()
834
835     def question(self, msg):
836         return QMessageBox.question(self, 'Message', msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
837
838     def show_message(self, msg):
839         QMessageBox.information(self, 'Message', msg, 'OK')
840
841     def password_dialog(self ):
842         d = QDialog(self)
843         d.setModal(1)
844
845         pw = QLineEdit()
846         pw.setEchoMode(2)
847
848         vbox = QVBoxLayout()
849         msg = 'Please enter your password'
850         vbox.addWidget(QLabel(msg))
851
852         grid = QGridLayout()
853         grid.setSpacing(8)
854         grid.addWidget(QLabel('Password'), 1, 0)
855         grid.addWidget(pw, 1, 1)
856         vbox.addLayout(grid)
857
858         vbox.addLayout(ok_cancel_buttons(d))
859         d.setLayout(vbox) 
860
861         if not d.exec_(): return
862         return unicode(pw.text())
863
864     @staticmethod
865     def change_password_dialog( wallet, parent=None ):
866
867         if not wallet.seed:
868             QMessageBox.information(parent, 'Message', 'No seed', 'OK')
869             return
870
871         d = QDialog(parent)
872         d.setModal(1)
873
874         pw = QLineEdit()
875         pw.setEchoMode(2)
876         new_pw = QLineEdit()
877         new_pw.setEchoMode(2)
878         conf_pw = QLineEdit()
879         conf_pw.setEchoMode(2)
880
881         vbox = QVBoxLayout()
882         if parent:
883             msg = 'Your wallet is encrypted. Use this dialog to change your password.\nTo disable wallet encryption, enter an empty new password.' if wallet.use_encryption else 'Your wallet keys are not encrypted'
884         else:
885             msg = "Please choose a password to encrypt your wallet keys.\nLeave these fields empty if you want to disable encryption."
886         vbox.addWidget(QLabel(msg))
887
888         grid = QGridLayout()
889         grid.setSpacing(8)
890
891         if wallet.use_encryption:
892             grid.addWidget(QLabel('Password'), 1, 0)
893             grid.addWidget(pw, 1, 1)
894
895         grid.addWidget(QLabel('New Password'), 2, 0)
896         grid.addWidget(new_pw, 2, 1)
897
898         grid.addWidget(QLabel('Confirm Password'), 3, 0)
899         grid.addWidget(conf_pw, 3, 1)
900         vbox.addLayout(grid)
901
902         vbox.addLayout(ok_cancel_buttons(d))
903         d.setLayout(vbox) 
904
905         if not d.exec_(): return
906
907         password = unicode(pw.text()) if wallet.use_encryption else None
908         new_password = unicode(new_pw.text())
909         new_password2 = unicode(conf_pw.text())
910
911         try:
912             seed = wallet.pw_decode( wallet.seed, password)
913         except:
914             QMessageBox.warning(parent, 'Error', 'Incorrect Password', 'OK')
915             return
916
917         if new_password != new_password2:
918             QMessageBox.warning(parent, 'Error', 'Passwords do not match', 'OK')
919             return
920
921         wallet.update_password(seed, new_password)
922
923     @staticmethod
924     def seed_dialog(wallet, parent=None):
925         d = QDialog(parent)
926         d.setModal(1)
927
928         vbox = QVBoxLayout()
929         msg = "Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet."
930         vbox.addWidget(QLabel(msg))
931
932         grid = QGridLayout()
933         grid.setSpacing(8)
934
935         seed_e = QLineEdit()
936         grid.addWidget(QLabel('Seed or mnemonic'), 1, 0)
937         grid.addWidget(seed_e, 1, 1)
938
939         gap_e = QLineEdit()
940         gap_e.setText("5")
941         grid.addWidget(QLabel('Gap limit'), 2, 0)
942         grid.addWidget(gap_e, 2, 1)
943         gap_e.textChanged.connect(lambda: numbify(gap_e,True))
944         vbox.addLayout(grid)
945
946         vbox.addLayout(ok_cancel_buttons(d))
947         d.setLayout(vbox) 
948
949         if not d.exec_(): return
950
951         try:
952             gap = int(unicode(gap_e.text()))
953         except:
954             QMessageBox.warning(None, 'Error', 'error', 'OK')
955             sys.exit(0)
956
957         try:
958             seed = unicode(seed_e.text())
959             seed.decode('hex')
960         except:
961             from electrum import mnemonic
962             print "not hex, trying decode"
963             try:
964                 seed = mnemonic.mn_decode( seed.split(' ') )
965             except:
966                 QMessageBox.warning(None, 'Error', 'I cannot decode this', 'OK')
967                 sys.exit(0)
968         if not seed:
969             QMessageBox.warning(None, 'Error', 'no seed', 'OK')
970             sys.exit(0)
971         
972         wallet.seed = str(seed)
973         #print repr(wallet.seed)
974         wallet.gap_limit = gap
975         return True
976
977
978     def settings_dialog(self):
979         d = QDialog(self)
980         d.setModal(1)
981
982         vbox = QVBoxLayout()
983
984         msg = 'Here are the settings of your wallet.'
985         vbox.addWidget(QLabel(msg))
986
987         grid = QGridLayout()
988         grid.setSpacing(8)
989         vbox.addLayout(grid)
990
991         fee_e = QLineEdit()
992         fee_e.setText("%s"% str( Decimal( self.wallet.fee)/100000000 ) )
993         grid.addWidget(QLabel('Fee per tx. input'), 2, 0)
994         grid.addWidget(fee_e, 2, 1)
995         fee_e.textChanged.connect(lambda: numbify(fee_e,False))
996
997         nz_e = QLineEdit()
998         nz_e.setText("%d"% self.wallet.num_zeros)
999         grid.addWidget(QLabel('Zeros displayed after decimal point'), 3, 0)
1000         grid.addWidget(nz_e, 3, 1)
1001         nz_e.textChanged.connect(lambda: numbify(nz_e,True))
1002
1003         vbox.addLayout(ok_cancel_buttons(d))
1004         d.setLayout(vbox) 
1005
1006         if not d.exec_(): return
1007
1008         fee = unicode(fee_e.text())
1009         try:
1010             fee = int( 100000000 * Decimal(fee) )
1011         except:
1012             QMessageBox.warning(self, 'Error', 'Invalid value:%s'%fee, 'OK')
1013             return
1014
1015         if self.wallet.fee != fee:
1016             self.wallet.fee = fee
1017             self.wallet.save()
1018         
1019         nz = unicode(nz_e.text())
1020         try:
1021             nz = int( nz )
1022             if nz>8: nz=8
1023         except:
1024             QMessageBox.warning(self, 'Error', 'Invalid value:%s'%nz, 'OK')
1025             return
1026
1027         if self.wallet.num_zeros != nz:
1028             self.wallet.num_zeros = nz
1029             self.update_history_tab()
1030             self.update_receive_tab()
1031             self.wallet.save()
1032
1033     @staticmethod 
1034     def network_dialog(wallet, parent=None):
1035         interface = wallet.interface
1036         if parent:
1037             if interface.is_connected:
1038                 status = "Connected to %s:%d\n%d blocks"%(interface.host, interface.port, wallet.blocks)
1039             else:
1040                 status = "Not connected"
1041             server = wallet.server
1042         else:
1043             import random
1044             status = "Please choose a server."
1045             server = random.choice( DEFAULT_SERVERS )
1046
1047         if not wallet.interface.servers:
1048             servers_list = []
1049             for x in DEFAULT_SERVERS:
1050                 h,port,protocol = x.split(':')
1051                 servers_list.append( (h,[(protocol,port)] ) )
1052         else:
1053             servers_list = wallet.interface.servers
1054             
1055         plist = {}
1056         for item in servers_list:
1057             host, pp = item
1058             z = {}
1059             for item2 in pp:
1060                 protocol, port = item2
1061                 z[protocol] = port
1062             plist[host] = z
1063
1064         d = QDialog(parent)
1065         d.setModal(1)
1066         d.setWindowTitle('Server')
1067         d.setMinimumSize(375, 20)
1068
1069         vbox = QVBoxLayout()
1070         vbox.setSpacing(20)
1071
1072         hbox = QHBoxLayout()
1073         l = QLabel()
1074         l.setPixmap(QPixmap(":icons/network.png"))
1075         hbox.addWidget(l)        
1076         hbox.addWidget(QLabel(status))
1077
1078         vbox.addLayout(hbox)
1079
1080         hbox = QHBoxLayout()
1081         host_line = QLineEdit()
1082         host_line.setText(server)
1083         hbox.addWidget(QLabel('Connect to:'))
1084         hbox.addWidget(host_line)
1085         vbox.addLayout(hbox)
1086
1087         hbox = QHBoxLayout()
1088
1089         buttonGroup = QGroupBox("protocol")
1090         radio1 = QRadioButton("tcp", buttonGroup)
1091         radio2 = QRadioButton("http", buttonGroup)
1092
1093         def current_line():
1094             return unicode(host_line.text()).split(':')
1095             
1096         def set_button(protocol):
1097             if protocol == 't':
1098                 radio1.setChecked(1)
1099             elif protocol == 'h':
1100                 radio2.setChecked(1)
1101
1102         def set_protocol(protocol):
1103             host = current_line()[0]
1104             pp = plist[host]
1105             if protocol not in pp.keys():
1106                 protocol = pp.keys()[0]
1107                 set_button(protocol)
1108             port = pp[protocol]
1109             host_line.setText( host + ':' + port + ':' + protocol)
1110
1111         radio1.clicked.connect(lambda x: set_protocol('t') )
1112         radio2.clicked.connect(lambda x: set_protocol('h') )
1113
1114         set_button(current_line()[2])
1115
1116         hbox.addWidget(QLabel('Protocol:'))
1117         hbox.addWidget(radio1)
1118         hbox.addWidget(radio2)
1119
1120         vbox.addLayout(hbox)
1121
1122         if wallet.interface.servers:
1123             label = 'Active Servers'
1124         else:
1125             label = 'Default Servers'
1126         
1127         servers_list_widget = QTreeWidget(parent)
1128         servers_list_widget.setHeaderLabels( [ label ] )
1129         servers_list_widget.setMaximumHeight(150)
1130         for host in plist.keys():
1131             servers_list_widget.addTopLevelItem(QTreeWidgetItem( [ host ] ))
1132
1133         def do_set_line(x):
1134             host = unicode(x.text(0))
1135             pp = plist[host]
1136             if 't' in pp.keys():
1137                 protocol = 't'
1138             else:
1139                 protocol = pp.keys()[0]
1140             port = pp[protocol]
1141             host_line.setText( host + ':' + port + ':' + protocol)
1142             set_button(protocol)
1143
1144         servers_list_widget.connect(servers_list_widget, SIGNAL('itemClicked(QTreeWidgetItem*, int)'), do_set_line)
1145         vbox.addWidget(servers_list_widget)
1146
1147         vbox.addLayout(ok_cancel_buttons(d))
1148         d.setLayout(vbox) 
1149
1150         if not d.exec_(): return
1151         server = unicode( host_line.text() )
1152
1153         try:
1154             wallet.set_server(server)
1155         except:
1156             QMessageBox.information(None, 'Error', 'error', 'OK')
1157             if parent == None:
1158                 sys.exit(1)
1159             else:
1160                 return
1161
1162         return True
1163
1164
1165
1166 class ElectrumGui():
1167
1168     def __init__(self, wallet):
1169         self.wallet = wallet
1170         self.app = QApplication(sys.argv)
1171
1172     def waiting_dialog(self):
1173
1174         s = Timer()
1175         s.start()
1176         w = QDialog()
1177         w.resize(200, 70)
1178         w.setWindowTitle('Electrum')
1179         l = QLabel('')
1180         vbox = QVBoxLayout()
1181         vbox.addWidget(l)
1182         w.setLayout(vbox)
1183         w.show()
1184         def f():
1185             if self.wallet.up_to_date: w.close()
1186             else:
1187                 l.setText("Please wait...\nGenerating addresses: %d"%len(self.wallet.all_addresses()))
1188                 pass
1189         w.connect(s, QtCore.SIGNAL('timersignal'), f)
1190         self.wallet.interface.poke()
1191         w.exec_()
1192         w.destroy()
1193
1194
1195     def restore_or_create(self):
1196
1197         msg = "Wallet file not found.\nDo you want to create a new wallet,\n or to restore an existing one?"
1198         r = QMessageBox.question(None, 'Message', msg, 'create', 'restore', 'cancel', 0, 2)
1199         if r==2: return False
1200         
1201         is_recovery = (r==1)
1202         wallet = self.wallet
1203         # ask for the server.
1204         if not ElectrumWindow.network_dialog( wallet, parent=None ): return False
1205
1206         if not is_recovery:
1207             wallet.new_seed(None)
1208             wallet.init_mpk( wallet.seed )
1209             wallet.up_to_date_event.clear()
1210             wallet.up_to_date = False
1211             self.waiting_dialog()
1212             # run a dialog indicating the seed, ask the user to remember it
1213             ElectrumWindow.show_seed_dialog(wallet)
1214             #ask for password
1215             ElectrumWindow.change_password_dialog(wallet)
1216         else:
1217             # ask for seed and gap.
1218             if not ElectrumWindow.seed_dialog( wallet ): return False
1219             wallet.init_mpk( wallet.seed )
1220             wallet.up_to_date_event.clear()
1221             wallet.up_to_date = False
1222             self.waiting_dialog()
1223             if wallet.is_found():
1224                 # history and addressbook
1225                 wallet.update_tx_history()
1226                 wallet.fill_addressbook()
1227                 print "recovery successful"
1228                 wallet.save()
1229             else:
1230                 QMessageBox.information(None, 'Message', "No transactions found for this seed", 'OK')
1231
1232         wallet.save()
1233         return True
1234
1235     def main(self,url):
1236         s = Timer()
1237         s.start()
1238         w = ElectrumWindow(self.wallet)
1239         if url: w.set_url(url)
1240         w.app = self.app
1241         w.connect_slots(s)
1242         w.update_wallet()
1243
1244         self.app.exec_()