separate non-modal window for QR codes (point of sale)
[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 from i18n import _
21 from util import print_error
22
23 try:
24     import PyQt4
25 except:
26     sys.exit("Error: Could not import PyQt4 on Linux systems, you may try 'sudo apt-get install python-qt4'")
27
28 from PyQt4.QtGui import *
29 from PyQt4.QtCore import *
30 import PyQt4.QtCore as QtCore
31 import PyQt4.QtGui as QtGui
32 from interface import DEFAULT_SERVERS
33
34 try:
35     import icons_rc
36 except:
37     sys.exit("Error: Could not import icons_rc.py, please generate it with: 'pyrcc4 icons.qrc -o lib/icons_rc.py'")
38
39 from wallet import format_satoshis
40 import bmp, mnemonic, pyqrnative, qrscanner
41
42 from decimal import Decimal
43
44 import platform
45
46 if platform.system() == 'Windows':
47     MONOSPACE_FONT = 'Lucida Console'
48 elif platform.system() == 'Darwin':
49     MONOSPACE_FONT = 'Monaco'
50 else:
51     MONOSPACE_FONT = 'monospace'
52
53 ALIAS_REGEXP = '^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$'    
54
55 def numbify(entry, is_int = False):
56     text = unicode(entry.text()).strip()
57     pos = entry.cursorPosition()
58     chars = '0123456789'
59     if not is_int: chars +='.'
60     s = ''.join([i for i in text if i in chars])
61     if not is_int:
62         if '.' in s:
63             p = s.find('.')
64             s = s.replace('.','')
65             s = s[:p] + '.' + s[p:p+8]
66         try:
67             amount = int( Decimal(s) * 100000000 )
68         except:
69             amount = None
70     else:
71         try:
72             amount = int( s )
73         except:
74             amount = None
75     entry.setText(s)
76     entry.setCursorPosition(pos)
77     return amount
78
79
80 class Timer(QtCore.QThread):
81     def run(self):
82         while True:
83             self.emit(QtCore.SIGNAL('timersignal'))
84             time.sleep(0.5)
85
86 class HelpButton(QPushButton):
87     def __init__(self, text):
88         QPushButton.__init__(self, '?')
89         self.setFocusPolicy(Qt.NoFocus)
90         self.setFixedWidth(20)
91         self.clicked.connect(lambda: QMessageBox.information(self, 'Help', text, 'OK') )
92
93
94 class EnterButton(QPushButton):
95     def __init__(self, text, func):
96         QPushButton.__init__(self, text)
97         self.func = func
98         self.clicked.connect(func)
99
100     def keyPressEvent(self, e):
101         if e.key() == QtCore.Qt.Key_Return:
102             apply(self.func,())
103
104 class MyTreeWidget(QTreeWidget):
105     def __init__(self, parent):
106         QTreeWidget.__init__(self, parent)
107         def ddfr(item):
108             if not item: return
109             for i in range(0,self.viewport().height()/5):
110                 if self.itemAt(QPoint(0,i*5)) == item:
111                     break
112             else:
113                 return
114             for j in range(0,30):
115                 if self.itemAt(QPoint(0,i*5 + j)) != item:
116                     break
117             self.emit(SIGNAL('customContextMenuRequested(const QPoint&)'), QPoint(50, i*5 + j - 1))
118
119         self.connect(self, SIGNAL('itemActivated(QTreeWidgetItem*, int)'), ddfr)
120         
121
122
123
124 class StatusBarButton(QPushButton):
125     def __init__(self, icon, tooltip, func):
126         QPushButton.__init__(self, icon, '')
127         self.setToolTip(tooltip)
128         self.setFlat(True)
129         self.setMaximumWidth(25)
130         self.clicked.connect(func)
131         self.func = func
132
133     def keyPressEvent(self, e):
134         if e.key() == QtCore.Qt.Key_Return:
135             apply(self.func,())
136
137
138 class QRCodeWidget(QWidget):
139
140     def __init__(self, addr):
141         super(QRCodeWidget, self).__init__()
142         self.setMinimumSize(250, 250)
143         self.set_addr(addr)
144
145     def set_addr(self, addr):
146         self.addr = addr
147         self.qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.L)
148         self.qr.addData(addr)
149         self.qr.make()
150         
151     def paintEvent(self, e):
152         if not self.addr: return
153         
154         qp = QtGui.QPainter()
155         qp.begin(self)
156         boxsize = 6
157         size = self.qr.getModuleCount()*boxsize
158         k = self.qr.getModuleCount()
159         black = QColor(0, 0, 0, 255)
160         white = QColor(255, 255, 255, 255)
161         for r in range(k):
162             for c in range(k):
163                 if self.qr.isDark(r, c):
164                     qp.setBrush(black)
165                     qp.setPen(black)
166                 else:
167                     qp.setBrush(white)
168                     qp.setPen(white)
169                 qp.drawRect(c*boxsize, r*boxsize, boxsize, boxsize)
170         qp.end()
171         
172
173
174 class QR_Window(QWidget):
175
176     def __init__(self):
177         QWidget.__init__(self)
178         self.setWindowTitle('Bitcoin Electrum')
179         self.setMinimumSize(800, 250)
180         self.address = ''
181         self.labe = ''
182         self.amount = 0
183         self.setFocusPolicy(QtCore.Qt.NoFocus)
184
185         main_box = QHBoxLayout()
186         
187         self.qrw = QRCodeWidget('')
188         main_box.addWidget(self.qrw)
189
190         vbox = QVBoxLayout()
191         main_box.addLayout(vbox)
192
193         main_box.addStretch(1)
194
195         self.address_label = QLabel("")
196         vbox.addWidget(self.address_label)
197
198         self.label_label = QLabel("")
199         vbox.addWidget(self.label_label)
200
201         self.amount_label = QLabel("")
202         vbox.addWidget(self.amount_label)
203
204         vbox.addStretch(1)
205
206         self.setLayout(main_box)
207
208     def do_save(self):
209         self.filename = "qrcode.bmp"
210         bmp.save_qrcode(self.qrw.qr, self.filename)
211
212     def set_content(self, addr, label, amount):
213         self.address = addr
214         address_text = "<span style='font-size: 18pt'>%s</span>" % addr if addr else ""
215         self.address_label.setText(address_text)
216
217         self.amount = amount
218         amount_text = "<span style='font-size: 18pt'>%s</span> <span style='font-size: 10pt'>BTC</span> " % format_satoshis(amount) if amount else ""
219         self.amount_label.setText(amount_text)
220
221         self.label = label
222         label_text = "<span style='font-size: 18pt'>%s</span>" % label if label else ""
223         self.label_label.setText(label_text)
224         self.update()
225
226     def update(self):
227         msg = 'bitcoin:'+self.address
228         if self.amount is not None:
229             msg += '?amount=%s'%(str( Decimal(self.amount) /100000000))
230         if self.label is not None:
231             msg += '&label=%s'%(self.label)
232             
233         self.qrw.set_addr( msg )
234         self.qrw.repaint()
235
236             
237
238
239 def waiting_dialog(f):
240
241     s = Timer()
242     s.start()
243     w = QDialog()
244     w.resize(200, 70)
245     w.setWindowTitle('Electrum')
246     l = QLabel('')
247     vbox = QVBoxLayout()
248     vbox.addWidget(l)
249     w.setLayout(vbox)
250     w.show()
251     def ff():
252         s = f()
253         if s: l.setText(s)
254         else: w.close()
255     w.connect(s, QtCore.SIGNAL('timersignal'), ff)
256     w.exec_()
257     w.destroy()
258
259
260 def ok_cancel_buttons(dialog):
261     hbox = QHBoxLayout()
262     hbox.addStretch(1)
263     b = QPushButton("OK")
264     hbox.addWidget(b)
265     b.clicked.connect(dialog.accept)
266     b = QPushButton("Cancel")
267     hbox.addWidget(b)
268     b.clicked.connect(dialog.reject)
269     return hbox
270
271
272 class ElectrumWindow(QMainWindow):
273
274     def __init__(self, wallet, config):
275         QMainWindow.__init__(self)
276         self.wallet = wallet
277         self.config = config
278         self.wallet.interface.register_callback('updated', self.update_callback)
279         self.wallet.interface.register_callback('connected', self.update_callback)
280         self.wallet.interface.register_callback('disconnected', self.update_callback)
281         self.wallet.interface.register_callback('disconnecting', self.update_callback)
282
283         self.detailed_view = config.get('qt_detailed_view', False)
284
285         self.qr_window = None
286         self.funds_error = False
287         self.completions = QStringListModel()
288
289         self.tabs = tabs = QTabWidget(self)
290         tabs.addTab(self.create_history_tab(), _('History') )
291         if self.wallet.seed:
292             tabs.addTab(self.create_send_tab(), _('Send') )
293         tabs.addTab(self.create_receive_tab(), _('Receive') )
294         tabs.addTab(self.create_contacts_tab(), _('Contacts') )
295         tabs.addTab(self.create_wall_tab(), _('Wall') )
296         tabs.setMinimumSize(600, 400)
297         tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
298         self.setCentralWidget(tabs)
299         self.create_status_bar()
300
301         g = self.config.get("winpos-qt",[100, 100, 840, 400])
302         self.setGeometry(g[0], g[1], g[2], g[3])
303         title = 'Electrum ' + self.wallet.electrum_version + '  -  ' + self.config.path
304         if not self.wallet.seed: title += ' [seedless]'
305         self.setWindowTitle( title )
306
307         QShortcut(QKeySequence("Ctrl+W"), self, self.close)
308         QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
309         QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
310         QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
311         
312         self.connect(self, QtCore.SIGNAL('updatesignal'), self.update_wallet)
313         #self.connect(self, SIGNAL('editamount'), self.edit_amount)
314         self.history_list.setFocus(True)
315
316         # dark magic fix by flatfly; https://bitcointalk.org/index.php?topic=73651.msg959913#msg959913
317         if platform.system() == 'Windows':
318             n = 3 if self.wallet.seed else 2
319             tabs.setCurrentIndex (n)
320             tabs.setCurrentIndex (0)
321
322
323     def connect_slots(self, sender):
324         if self.wallet.seed:
325             self.connect(sender, QtCore.SIGNAL('timersignal'), self.check_recipient)
326             self.previous_payto_e=''
327
328     def check_recipient(self):
329         if self.payto_e.hasFocus():
330             return
331         r = unicode( self.payto_e.text() )
332         if r != self.previous_payto_e:
333             self.previous_payto_e = r
334             r = r.strip()
335             if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r):
336                 try:
337                     to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
338                 except:
339                     return
340                 if to_address:
341                     s = r + '  <' + to_address + '>'
342                     self.payto_e.setText(s)
343
344
345     def update_callback(self):
346         self.emit(QtCore.SIGNAL('updatesignal'))
347
348     def update_wallet(self):
349         if self.wallet.interface and self.wallet.interface.is_connected:
350             if not self.wallet.up_to_date:
351                 text = _( "Synchronizing..." )
352                 icon = QIcon(":icons/status_waiting.png")
353             else:
354                 c, u = self.wallet.get_balance()
355                 text =  _( "Balance" ) + ": %s "%( format_satoshis(c,False,self.wallet.num_zeros) )
356                 if u: text +=  "[%s unconfirmed]"%( format_satoshis(u,True,self.wallet.num_zeros).strip() )
357                 icon = QIcon(":icons/status_connected.png")
358         else:
359             text = _( "Not connected" )
360             icon = QIcon(":icons/status_disconnected.png")
361
362         if self.funds_error:
363             text = _( "Not enough funds" )
364
365         self.statusBar().showMessage(text)
366         self.status_button.setIcon( icon )
367
368         if self.wallet.up_to_date or not self.wallet.interface.is_connected:
369             self.textbox.setText( self.wallet.banner )
370             self.update_history_tab()
371             self.update_receive_tab()
372             self.update_contacts_tab()
373             self.update_completions()
374
375
376     def create_history_tab(self):
377         self.history_list = l = MyTreeWidget(self)
378         l.setColumnCount(5)
379         l.setColumnWidth(0, 40) 
380         l.setColumnWidth(1, 140) 
381         l.setColumnWidth(2, 350) 
382         l.setColumnWidth(3, 140) 
383         l.setColumnWidth(4, 140) 
384         l.setHeaderLabels( [ '', _( 'Date' ), _( 'Description' ) , _('Amount'), _('Balance')] )
385         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
386         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
387
388         l.setContextMenuPolicy(Qt.CustomContextMenu)
389         l.customContextMenuRequested.connect(self.create_history_menu)
390         return l
391
392
393     def create_history_menu(self, position):
394         self.history_list.selectedIndexes() 
395         item = self.history_list.currentItem()
396         if not item: return
397         tx_hash = str(item.toolTip(0))
398         if not tx_hash: return
399         menu = QMenu()
400         menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
401         menu.addAction(_("Details"), lambda: self.tx_details(tx_hash))
402         menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
403         menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
404
405
406     def tx_details(self, tx_hash):
407         tx_details = self.wallet.get_tx_details(tx_hash)
408         QMessageBox.information(self, 'Details', tx_details, 'OK')
409
410
411     def tx_label_clicked(self, item, column):
412         if column==2 and item.isSelected():
413             tx_hash = str(item.toolTip(0))
414             self.is_edit=True
415             #if not self.wallet.labels.get(tx_hash): item.setText(2,'')
416             item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
417             self.history_list.editItem( item, column )
418             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
419             self.is_edit=False
420
421     def tx_label_changed(self, item, column):
422         if self.is_edit: 
423             return
424         self.is_edit=True
425         tx_hash = str(item.toolTip(0))
426         tx = self.wallet.transactions.get(tx_hash)
427         s = self.wallet.labels.get(tx_hash)
428         text = unicode( item.text(2) )
429         if text: 
430             self.wallet.labels[tx_hash] = text
431             item.setForeground(2, QBrush(QColor('black')))
432         else:
433             if s: self.wallet.labels.pop(tx_hash)
434             text = self.wallet.get_default_label(tx_hash)
435             item.setText(2, text)
436             item.setForeground(2, QBrush(QColor('gray')))
437         self.is_edit=False
438
439
440     def edit_label(self, is_recv):
441         l = self.receive_list if is_recv else self.contacts_list
442         c = 2 if is_recv else 1
443         item = l.currentItem()
444         item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
445         l.editItem( item, c )
446         item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
447
448
449
450     def address_label_clicked(self, item, column, l, column_addr, column_label):
451         if column == column_label and item.isSelected():
452             addr = unicode( item.text(column_addr) )
453             label = unicode( item.text(column_label) )
454             if label in self.wallet.aliases.keys():
455                 return
456             item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
457             l.editItem( item, column )
458             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
459
460
461     def address_label_changed(self, item, column, l, column_addr, column_label):
462
463         if column == column_label:
464             addr = unicode( item.text(column_addr) )
465             text = unicode( item.text(column_label) )
466             changed = False
467
468             if text:
469                 if text not in self.wallet.aliases.keys():
470                     old_addr = self.wallet.labels.get(text)
471                     if old_addr != addr:
472                         self.wallet.labels[addr] = text
473                         changed = True
474                 else:
475                     print_error("Error: This is one of your aliases")
476                     label = self.wallet.labels.get(addr,'')
477                     item.setText(column_label, QString(label))
478             else:
479                 s = self.wallet.labels.get(addr)
480                 if s: 
481                     self.wallet.labels.pop(addr)
482                     changed = True
483
484             if changed:
485                 self.update_history_tab()
486                 self.update_completions()
487                 
488             self.recv_changed(item)
489
490
491     def recv_changed(self, a):
492         "current item changed"
493         if a is not None and self.qr_window:
494             address = str(a.text(1))
495             label = self.wallet.labels.get(address)
496             amount = self.wallet.requested_amounts.get(address)
497             self.qr_window.set_content( address, label, amount )
498
499
500     def update_history_tab(self):
501
502         self.history_list.clear()
503         for item in self.wallet.get_tx_history():
504             tx_hash, conf, is_mine, value, fee, balance, timestamp = item
505             if conf:
506                 try:
507                     time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
508                 except:
509                     time_str = "unknown"
510                 if conf == -1:
511                     icon = None
512                 if conf == 0:
513                     icon = QIcon(":icons/unconfirmed.png")
514                 elif conf < 6:
515                     icon = QIcon(":icons/clock%d.png"%conf)
516                 else:
517                     icon = QIcon(":icons/confirmed.png")
518             else:
519                 time_str = 'pending'
520                 icon = QIcon(":icons/unconfirmed.png")
521
522             if value is not None:
523                 v_str = format_satoshis(value, True, self.wallet.num_zeros)
524             else:
525                 v_str = '--'
526
527             balance_str = format_satoshis(balance, False, self.wallet.num_zeros)
528             
529             if tx_hash:
530                 label, is_default_label = self.wallet.get_label(tx_hash)
531             else:
532                 label = _('Pruned transaction outputs')
533                 is_default_label = False
534
535             item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
536             item.setFont(2, QFont(MONOSPACE_FONT))
537             item.setFont(3, QFont(MONOSPACE_FONT))
538             item.setFont(4, QFont(MONOSPACE_FONT))
539             if tx_hash:
540                 item.setToolTip(0, tx_hash)
541             if is_default_label:
542                 item.setForeground(2, QBrush(QColor('grey')))
543
544             item.setIcon(0, icon)
545             self.history_list.insertTopLevelItem(0,item)
546             
547
548         self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
549
550
551     def create_send_tab(self):
552         w = QWidget()
553
554         grid = QGridLayout()
555         grid.setSpacing(8)
556         grid.setColumnMinimumWidth(3,300)
557         grid.setColumnStretch(5,1)
558
559         self.payto_e = QLineEdit()
560         grid.addWidget(QLabel(_('Pay to')), 1, 0)
561         grid.addWidget(self.payto_e, 1, 1, 1, 3)
562         
563         def fill_from_qr():
564             qrcode = qrscanner.scan_qr()
565             if 'address' in qrcode:
566                 self.payto_e.setText(qrcode['address'])
567             if 'amount' in qrcode:
568                 self.amount_e.setText(str(qrcode['amount']))
569             if 'label' in qrcode:
570                 self.message_e.setText(qrcode['label'])
571             if 'message' in qrcode:
572                 self.message_e.setText("%s (%s)" % (self.message_e.text(), qrcode['message']))
573                 
574
575         if qrscanner.is_available():
576             b = QPushButton(_("Scan QR code"))
577             b.clicked.connect(fill_from_qr)
578             grid.addWidget(b, 1, 5)
579     
580         grid.addWidget(HelpButton(_('Recipient of the funds.') + '\n\n' + _('You may enter a Bitcoin address, a label from your list of contacts (a list of completions will be proposed), or an alias (email-like address that forwards to a Bitcoin address)')), 1, 4)
581
582         completer = QCompleter()
583         completer.setCaseSensitivity(False)
584         self.payto_e.setCompleter(completer)
585         completer.setModel(self.completions)
586
587         self.message_e = QLineEdit()
588         grid.addWidget(QLabel(_('Description')), 2, 0)
589         grid.addWidget(self.message_e, 2, 1, 1, 3)
590         grid.addWidget(HelpButton(_('Description of the transaction (not mandatory).') + '\n\n' + _('The description is not sent to the recipient of the funds. It is stored in your wallet file, and displayed in the \'History\' tab.')), 2, 4)
591
592         self.amount_e = QLineEdit()
593         grid.addWidget(QLabel(_('Amount')), 3, 0)
594         grid.addWidget(self.amount_e, 3, 1, 1, 2)
595         grid.addWidget(HelpButton(
596                 _('Amount to be sent.') + '\n\n' \
597                     + _('The amount will be displayed in red if you do not have enough funds in your wallet. Note that if you have frozen some of your addresses, the available funds will be lower than your total balance.')), 3, 3)
598         
599         self.fee_e = QLineEdit()
600         grid.addWidget(QLabel(_('Fee')), 4, 0)
601         grid.addWidget(self.fee_e, 4, 1, 1, 2) 
602         grid.addWidget(HelpButton(
603                 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
604                     + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
605                     + _('A suggested fee is automatically added to this field. You may override it. The suggested fee increases with the size of the transaction.')), 4, 3)
606         
607         b = EnterButton(_("Send"), self.do_send)
608         grid.addWidget(b, 6, 1)
609
610         b = EnterButton(_("Clear"),self.do_clear)
611         grid.addWidget(b, 6, 2)
612
613         self.payto_sig = QLabel('')
614         grid.addWidget(self.payto_sig, 7, 0, 1, 4)
615
616         QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
617         QShortcut(QKeySequence("Down"), w, w.focusNextChild)
618         w.setLayout(grid) 
619
620         w2 = QWidget()
621         vbox = QVBoxLayout()
622         vbox.addWidget(w)
623         vbox.addStretch(1)
624         w2.setLayout(vbox)
625
626         def entry_changed( is_fee ):
627             self.funds_error = False
628             amount = numbify(self.amount_e)
629             fee = numbify(self.fee_e)
630             if not is_fee: fee = None
631             if amount is None:
632                 return
633             inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
634             if not is_fee:
635                 self.fee_e.setText( str( Decimal( fee ) / 100000000 ) )
636             if inputs:
637                 palette = QPalette()
638                 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
639             else:
640                 palette = QPalette()
641                 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
642                 self.funds_error = True
643             self.amount_e.setPalette(palette)
644             self.fee_e.setPalette(palette)
645
646         self.amount_e.textChanged.connect(lambda: entry_changed(False) )
647         self.fee_e.textChanged.connect(lambda: entry_changed(True) )
648
649         return w2
650
651
652     def update_completions(self):
653         l = []
654         for addr,label in self.wallet.labels.items():
655             if addr in self.wallet.addressbook:
656                 l.append( label + '  <' + addr + '>')
657         l = l + self.wallet.aliases.keys()
658
659         self.completions.setStringList(l)
660
661
662
663     def do_send(self):
664
665         label = unicode( self.message_e.text() )
666         r = unicode( self.payto_e.text() )
667         r = r.strip()
668
669         # alias
670         m1 = re.match(ALIAS_REGEXP, r)
671         # label or alias, with address in brackets
672         m2 = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
673         
674         if m1:
675             to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
676             if not to_address:
677                 return
678         elif m2:
679             to_address = m2.group(2)
680         else:
681             to_address = r
682
683         if not self.wallet.is_valid(to_address):
684             QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
685             return
686
687         try:
688             amount = int( Decimal( unicode( self.amount_e.text())) * 100000000 )
689         except:
690             QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
691             return
692         try:
693             fee = int( Decimal( unicode( self.fee_e.text())) * 100000000 )
694         except:
695             QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
696             return
697
698         if self.wallet.use_encryption:
699             password = self.password_dialog()
700             if not password:
701                 return
702         else:
703             password = None
704
705         try:
706             tx = self.wallet.mktx( to_address, amount, label, password, fee)
707         except BaseException, e:
708             self.show_message(str(e))
709             return
710             
711         h = self.wallet.send_tx(tx)
712         waiting_dialog(lambda: False if self.wallet.tx_event.isSet() else _("Please wait..."))
713         status, msg = self.wallet.receive_tx( h )
714
715         if status:
716             QMessageBox.information(self, '', _('Payment sent.')+'\n'+msg, _('OK'))
717             self.do_clear()
718             self.update_contacts_tab()
719         else:
720             QMessageBox.warning(self, _('Error'), msg, _('OK'))
721
722
723     def set_url(self, url):
724         payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question)
725         self.tabs.setCurrentIndex(1)
726         label = self.wallet.labels.get(payto)
727         m_addr = label + '  <'+ payto+'>' if label else payto
728         self.payto_e.setText(m_addr)
729
730         self.message_e.setText(message)
731         self.amount_e.setText(amount)
732         if identity:
733             self.set_frozen(self.payto_e,True)
734             self.set_frozen(self.amount_e,True)
735             self.set_frozen(self.message_e,True)
736             self.payto_sig.setText( '      The bitcoin URI was signed by ' + identity )
737         else:
738             self.payto_sig.setVisible(False)
739
740     def do_clear(self):
741         self.payto_sig.setVisible(False)
742         for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
743             e.setText('')
744             self.set_frozen(e,False)
745
746     def set_frozen(self,entry,frozen):
747         if frozen:
748             entry.setReadOnly(True)
749             entry.setFrame(False)
750             palette = QPalette()
751             palette.setColor(entry.backgroundRole(), QColor('lightgray'))
752             entry.setPalette(palette)
753         else:
754             entry.setReadOnly(False)
755             entry.setFrame(True)
756             palette = QPalette()
757             palette.setColor(entry.backgroundRole(), QColor('white'))
758             entry.setPalette(palette)
759
760
761     def toggle_freeze(self,addr):
762         if not addr: return
763         if addr in self.wallet.frozen_addresses:
764             self.wallet.unfreeze(addr)
765         else:
766             self.wallet.freeze(addr)
767         self.update_receive_tab()
768
769     def toggle_priority(self,addr):
770         if not addr: return
771         if addr in self.wallet.prioritized_addresses:
772             self.wallet.unprioritize(addr)
773         else:
774             self.wallet.prioritize(addr)
775         self.update_receive_tab()
776
777
778     def create_list_tab(self, headers):
779         "generic tab creation method"
780         l = MyTreeWidget(self)
781         l.setColumnCount( len(headers) )
782         l.setHeaderLabels( headers )
783
784         w = QWidget()
785         vbox = QVBoxLayout()
786         w.setLayout(vbox)
787
788         vbox.setMargin(0)
789         vbox.setSpacing(0)
790         vbox.addWidget(l)
791         buttons = QWidget()
792         vbox.addWidget(buttons)
793
794         hbox = QHBoxLayout()
795         hbox.setMargin(0)
796         hbox.setSpacing(0)
797         buttons.setLayout(hbox)
798
799         return l,w,hbox
800
801
802     def create_receive_tab(self):
803         l,w,hbox = self.create_list_tab([_('Flags'), _('Address'), _('Label'), _('Requested'), _('Balance'), _('Tx')])
804         l.setContextMenuPolicy(Qt.CustomContextMenu)
805         l.customContextMenuRequested.connect(self.create_receive_menu)
806         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,1,2))
807         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,1,2))
808         self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.recv_changed(a))
809         self.receive_list = l
810         self.receive_buttons_hbox = hbox
811         self.qr_button = EnterButton(self.qr_button_text(), self.toggle_QR_window)
812         hbox.addWidget(self.qr_button)
813         self.print_button = EnterButton(_("Print QR"), self.print_qr)
814         self.print_button.setHidden(True)
815         hbox.addWidget(self.print_button)
816
817         self.details_button = EnterButton(self.details_button_text(), self.toggle_detailed_view)
818         hbox.addWidget(self.details_button)
819         hbox.addStretch(1)
820         return w
821
822     def print_qr(self):
823         if self.qr_window:
824             self.qr_window.do_save()
825             self.show_message(_("QR code saved to file") + " " + self.qr_window.filename)
826
827
828     def request_amount_dialog(self, address):
829         # pick the first unused address
830         # print "please wait" in qr window
831         # enter amount in usd
832
833         d = QDialog()
834         d.setWindowTitle('Request payment')
835
836         vbox = QVBoxLayout()
837         vbox.addWidget(QLabel(address))
838
839         grid = QGridLayout()
840         grid.setSpacing(8)
841         vbox.addLayout(grid)
842
843         try:
844             index = self.wallet.addresses.index(address)
845         except:
846             return
847
848         label = self.wallet.labels.get(address,'invoice %04d'%(index+1))
849         grid.addWidget(QLabel(_('Label')), 0, 0)
850         label_e = QLineEdit()
851         label_e.setText(label)
852         grid.addWidget(label_e, 0, 1)
853
854         amount_e = QLineEdit()
855         amount_e.textChanged.connect(lambda: numbify(amount_e))
856
857         grid.addWidget(QLabel(_('Amount')), 1, 0)
858         grid.addWidget(amount_e, 1, 1, 1, 3)
859
860         vbox.addLayout(ok_cancel_buttons(d))
861         d.setLayout(vbox)
862
863         amount_e.setFocus()
864         if not d.exec_(): return
865         try:
866             amount = int( Decimal( unicode( amount_e.text())) * 100000000 )
867         except:
868             return
869         self.wallet.requested_amounts[address] = amount
870
871         label = unicode(label_e.text())
872         if label:
873             self.wallet.labels[address] = label
874
875         self.update_receive_item(self.receive_list.currentItem())
876
877         if self.qr_window:
878             self.qr_window.set_content( address, label, amount )
879             #print "raise"
880             #self.raise_()
881             #self.receive_list.currentItem().setFocus(True)
882
883         
884     def details_button_text(self):
885         return _('Hide details') if self.detailed_view else _('Show details')
886
887     def qr_button_text(self):
888         return _('Hide QR') if self.qr_window else _('Show QR')
889
890
891     def toggle_detailed_view(self):
892         self.detailed_view = not self.detailed_view
893         self.config.set_key('qt_detailed_view', self.detailed_view, True)
894
895         self.details_button.setText(self.details_button_text())
896         self.wallet.save()
897         self.update_receive_tab()
898         self.update_contacts_tab()
899
900
901     def create_contacts_tab(self):
902         l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
903         l.setContextMenuPolicy(Qt.CustomContextMenu)
904         l.customContextMenuRequested.connect(self.create_contact_menu)
905         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
906         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
907         self.contacts_list = l
908         self.contacts_buttons_hbox = hbox
909         hbox.addWidget(EnterButton(_("New"), self.new_contact_dialog))
910         hbox.addStretch(1)
911         return w
912
913
914     def create_receive_menu(self, position):
915         # fixme: this function apparently has a side effect.
916         # if it is not called the menu pops up several times
917         #self.receive_list.selectedIndexes() 
918
919         item = self.receive_list.itemAt(position)
920         if not item: return
921         addr = unicode(item.text(1))
922         menu = QMenu()
923         menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
924         if self.qr_window: menu.addAction(_("Request payment"), lambda: self.request_amount_dialog(addr))
925         menu.addAction(_("Edit label"), lambda: self.edit_label(True))
926         menu.addAction(_("Sign message"), lambda: self.sign_message(addr))
927
928         t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
929         menu.addAction(t, lambda: self.toggle_freeze(addr))
930         t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
931         menu.addAction(t, lambda: self.toggle_priority(addr))
932         menu.exec_(self.receive_list.viewport().mapToGlobal(position))
933
934
935     def payto(self, x, is_alias):
936         if not x: return
937         if is_alias:
938             label = x
939             m_addr = label
940         else:
941             addr = x
942             label = self.wallet.labels.get(addr)
943             m_addr = label + '  <' + addr + '>' if label else addr
944         self.tabs.setCurrentIndex(1)
945         self.payto_e.setText(m_addr)
946         self.amount_e.setFocus()
947
948     def delete_contact(self, x, is_alias):
949         if self.question("Do you want to remove %s from your list of contacts?"%x):
950             if not is_alias and x in self.wallet.addressbook:
951                 self.wallet.addressbook.remove(x)
952                 if x in self.wallet.labels.keys():
953                     self.wallet.labels.pop(x)
954             elif is_alias and x in self.wallet.aliases:
955                 self.wallet.aliases.pop(x)
956             self.update_history_tab()
957             self.update_contacts_tab()
958             self.update_completions()
959
960     def create_contact_menu(self, position):
961         # fixme: this function apparently has a side effect.
962         # if it is not called the menu pops up several times
963         #self.contacts_list.selectedIndexes() 
964
965         item = self.contacts_list.itemAt(position)
966         if not item: return
967         addr = unicode(item.text(0))
968         label = unicode(item.text(1))
969         is_alias = label in self.wallet.aliases.keys()
970         x = label if is_alias else addr
971         menu = QMenu()
972         menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
973         menu.addAction(_("Pay to"), lambda: self.payto(x, is_alias))
974         menu.addAction(_("View QR code"),lambda: self.show_address_qrcode(addr))
975         if not is_alias:
976             menu.addAction(_("Edit label"), lambda: self.edit_label(False))
977         else:
978             menu.addAction(_("View alias details"), lambda: self.show_contact_details(label))
979         menu.addAction(_("Delete"), lambda: self.delete_contact(x,is_alias))
980         menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
981
982
983     def update_receive_item(self, item):
984         address = str( item.data(1,0).toString() )
985
986         flags = self.wallet.get_address_flags(address)
987         item.setData(0,0,flags)
988
989         label = self.wallet.labels.get(address,'')
990         item.setData(2,0,label)
991
992         amount_str = format_satoshis( self.wallet.requested_amounts.get(address,0) )
993         item.setData(3,0,amount_str)
994         
995         c, u = self.wallet.get_addr_balance(address)
996         balance = format_satoshis( c + u, False, self.wallet.num_zeros )
997         item.setData(4,0,balance)
998
999         if address in self.wallet.frozen_addresses: 
1000             item.setBackgroundColor(1, QColor('lightblue'))
1001         elif address in self.wallet.prioritized_addresses: 
1002             item.setBackgroundColor(1, QColor('lightgreen'))
1003         
1004
1005     def update_receive_tab(self):
1006         l = self.receive_list
1007         
1008         l.clear()
1009         l.setColumnHidden(0, not self.detailed_view)
1010         l.setColumnHidden(3, self.qr_window is None)
1011         l.setColumnHidden(4, not self.detailed_view)
1012         l.setColumnHidden(5, not self.detailed_view)
1013         l.setColumnWidth(0, 50)
1014         l.setColumnWidth(1, 310) 
1015         l.setColumnWidth(2, 200)
1016         l.setColumnWidth(3, 130)
1017         l.setColumnWidth(4, 130)
1018         l.setColumnWidth(5, 10)
1019
1020         gap = 0
1021         is_red = False
1022         for address in self.wallet.all_addresses():
1023
1024             if self.wallet.is_change(address) and not self.detailed_view:
1025                 continue
1026
1027             n = 0 
1028             h = self.wallet.history.get(address,[])
1029
1030             if h != ['*']: 
1031                 for tx_hash, tx_height in h:
1032                     tx = self.wallet.transactions.get(tx_hash)
1033                     if tx: n += 1
1034                 num_tx = "%d "%n
1035             else:
1036                 n = -1
1037                 num_tx = "*"
1038
1039             if n==0:
1040                 if address in self.wallet.addresses:
1041                     gap += 1
1042                     if gap > self.wallet.gap_limit:
1043                         is_red = True
1044             else:
1045                 if address in self.wallet.addresses:
1046                     gap = 0
1047
1048             item = QTreeWidgetItem( [ '', address, '', '', '', num_tx] )
1049             item.setFont(0, QFont(MONOSPACE_FONT))
1050             item.setFont(1, QFont(MONOSPACE_FONT))
1051             item.setFont(3, QFont(MONOSPACE_FONT))
1052             self.update_receive_item(item)
1053             if is_red and address in self.wallet.addresses:
1054                 item.setBackgroundColor(1, QColor('red'))
1055             l.addTopLevelItem(item)
1056
1057         # we use column 1 because column 0 may be hidden
1058         l.setCurrentItem(l.topLevelItem(0),1)
1059
1060     def show_contact_details(self, m):
1061         a = self.wallet.aliases.get(m)
1062         if a:
1063             if a[0] in self.wallet.authorities.keys():
1064                 s = self.wallet.authorities.get(a[0])
1065             else:
1066                 s = "self-signed"
1067             msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0]
1068             QMessageBox.information(self, 'Alias', msg, 'OK')
1069
1070     def update_contacts_tab(self):
1071
1072         l = self.contacts_list
1073         l.clear()
1074         l.setColumnHidden(2, not self.detailed_view)
1075         l.setColumnWidth(0, 350) 
1076         l.setColumnWidth(1, 330)
1077         l.setColumnWidth(2, 100) 
1078
1079         alias_targets = []
1080         for alias, v in self.wallet.aliases.items():
1081             s, target = v
1082             alias_targets.append(target)
1083             item = QTreeWidgetItem( [ target, alias, '-'] )
1084             item.setBackgroundColor(0, QColor('lightgray'))
1085             l.addTopLevelItem(item)
1086             
1087         for address in self.wallet.addressbook:
1088             if address in alias_targets: continue
1089             label = self.wallet.labels.get(address,'')
1090             n = 0 
1091             for item in self.wallet.transactions.values():
1092                 if address in item['outputs'] : n=n+1
1093             tx = "%d"%n
1094             item = QTreeWidgetItem( [ address, label, tx] )
1095             item.setFont(0, QFont(MONOSPACE_FONT))
1096             l.addTopLevelItem(item)
1097
1098         l.setCurrentItem(l.topLevelItem(0))
1099
1100     def create_wall_tab(self):
1101         self.textbox = textbox = QTextEdit(self)
1102         textbox.setFont(QFont(MONOSPACE_FONT))
1103         textbox.setReadOnly(True)
1104         return textbox
1105
1106     def create_status_bar(self):
1107         sb = QStatusBar()
1108         sb.setFixedHeight(35)
1109         if self.wallet.seed:
1110             sb.addPermanentWidget( StatusBarButton( QIcon(":icons/lock.png"), "Password", lambda: self.change_password_dialog(self.wallet, self) ) )
1111         sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), "Preferences", self.settings_dialog ) )
1112         if self.wallet.seed:
1113             sb.addPermanentWidget( StatusBarButton( QIcon(":icons/seed.png"), "Seed", lambda: self.show_seed_dialog(self.wallet, self) ) )
1114         self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), "Network", lambda: self.network_dialog(self.wallet, self) ) 
1115         sb.addPermanentWidget( self.status_button )
1116         self.setStatusBar(sb)
1117
1118     def new_contact_dialog(self):
1119         text, ok = QInputDialog.getText(self, _('New Contact'), _('Address') + ':')
1120         address = unicode(text)
1121         if ok:
1122             if self.wallet.is_valid(address):
1123                 self.wallet.addressbook.append(address)
1124                 self.wallet.save()
1125                 self.update_contacts_tab()
1126                 self.update_history_tab()
1127                 self.update_completions()
1128             else:
1129                 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1130
1131     @staticmethod
1132     def show_seed_dialog(wallet, parent=None):
1133         if not wallet.seed:
1134             QMessageBox.information(parent, _('Message'),
1135                                     _('No seed'), _('OK'))
1136             return
1137
1138         if wallet.use_encryption:
1139             password = parent.password_dialog()
1140             if not password:
1141                 return
1142         else:
1143             password = None
1144             
1145         try:
1146             seed = wallet.pw_decode(wallet.seed, password)
1147         except:
1148             QMessageBox.warning(parent, _('Error'),
1149                                 _('Incorrect Password'), _('OK'))
1150             return
1151
1152         dialog = QDialog(None)
1153         dialog.setModal(1)
1154         dialog.setWindowTitle("Electrum")
1155
1156         brainwallet = ' '.join(mnemonic.mn_encode(seed))
1157
1158         msg =   _("Your wallet generation seed is") +":<p>\"" + brainwallet + "\"<p>" \
1159               + _("Please write down or memorize these 12 words (order is important).") + " " \
1160               + _("This seed will allow you to recover your wallet in case of computer failure.") + "<p>" \
1161               + _("WARNING: Never disclose your seed. Never type it on a website.") + "<p>"
1162
1163         main_text = QLabel(msg)
1164         main_text.setWordWrap(True)
1165
1166         logo = QLabel()
1167         logo.setPixmap(QPixmap(":icons/seed.png").scaledToWidth(56))
1168
1169         if parent:
1170             app = parent.app
1171         else:
1172             app = QApplication
1173
1174         copy_function = lambda: app.clipboard().setText(brainwallet)
1175         copy_button = QPushButton(_("Copy to Clipboard"))
1176         copy_button.clicked.connect(copy_function)
1177
1178         show_qr_function = lambda: ElectrumWindow.show_seed_qrcode(seed)
1179         qr_button = QPushButton(_("View as QR Code"))
1180         qr_button.clicked.connect(show_qr_function)
1181
1182         ok_button = QPushButton(_("OK"))
1183         ok_button.setDefault(True)
1184         ok_button.clicked.connect(dialog.accept)
1185
1186         main_layout = QGridLayout()
1187         main_layout.addWidget(logo, 0, 0)
1188         main_layout.addWidget(main_text, 0, 1, 1, -1)
1189         main_layout.addWidget(copy_button, 1, 1)
1190         main_layout.addWidget(qr_button, 1, 2)
1191         main_layout.addWidget(ok_button, 1, 3)
1192         dialog.setLayout(main_layout)
1193
1194         dialog.exec_()
1195
1196     @staticmethod
1197     def show_seed_qrcode(seed):
1198         if not seed: return
1199         d = QDialog(None)
1200         d.setModal(1)
1201         d.setWindowTitle(_("Seed"))
1202         d.setMinimumSize(270, 300)
1203         vbox = QVBoxLayout()
1204         vbox.addWidget(QRCodeWidget(seed))
1205         hbox = QHBoxLayout()
1206         hbox.addStretch(1)
1207         b = QPushButton(_("OK"))
1208         hbox.addWidget(b)
1209         b.clicked.connect(d.accept)
1210
1211         vbox.addLayout(hbox)
1212         d.setLayout(vbox)
1213         d.exec_()
1214
1215     def sign_message(self,address):
1216         if not address: return
1217         d = QDialog(self)
1218         d.setModal(1)
1219         d.setWindowTitle('Sign Message')
1220         d.setMinimumSize(270, 350)
1221
1222         tab_widget = QTabWidget()
1223         tab = QWidget()
1224         layout = QGridLayout(tab)
1225
1226         sign_address = QLineEdit()
1227         sign_address.setText(address)
1228         layout.addWidget(QLabel(_('Address')), 1, 0)
1229         layout.addWidget(sign_address, 1, 1)
1230
1231         sign_message = QTextEdit()
1232         layout.addWidget(QLabel(_('Message')), 2, 0)
1233         layout.addWidget(sign_message, 2, 1, 2, 1)
1234
1235         sign_signature = QLineEdit()
1236         layout.addWidget(QLabel(_('Signature')), 3, 0)
1237         layout.addWidget(sign_signature, 3, 1)
1238
1239         def do_sign():
1240             if self.wallet.use_encryption:
1241                 password = self.password_dialog()
1242                 if not password:
1243                     return
1244             else:
1245                 password = None
1246
1247             try:
1248                 signature = self.wallet.sign_message(sign_address.text(), sign_message.toPlainText(), password)
1249                 sign_signature.setText(signature)
1250             except BaseException, e:
1251                 self.show_message(str(e))
1252                 return
1253
1254         hbox = QHBoxLayout()
1255         b = QPushButton(_("Sign"))
1256         hbox.addWidget(b)
1257         b.clicked.connect(do_sign)
1258         b = QPushButton(_("Close"))
1259         b.clicked.connect(d.accept)
1260         hbox.addWidget(b)
1261         layout.addLayout(hbox, 4, 1)
1262         tab_widget.addTab(tab, "Sign")
1263
1264
1265         tab = QWidget()
1266         layout = QGridLayout(tab)
1267
1268         verify_address = QLineEdit()
1269         layout.addWidget(QLabel(_('Address')), 1, 0)
1270         layout.addWidget(verify_address, 1, 1)
1271
1272         verify_message = QTextEdit()
1273         layout.addWidget(QLabel(_('Message')), 2, 0)
1274         layout.addWidget(verify_message, 2, 1, 2, 1)
1275
1276         verify_signature = QLineEdit()
1277         layout.addWidget(QLabel(_('Signature')), 3, 0)
1278         layout.addWidget(verify_signature, 3, 1)
1279
1280         def do_verify():
1281             try:
1282                 self.wallet.verify_message(verify_address.text(), verify_signature.text(), verify_message.toPlainText())
1283                 self.show_message("Signature verified")
1284             except BaseException, e:
1285                 self.show_message(str(e))
1286                 return
1287
1288         hbox = QHBoxLayout()
1289         b = QPushButton(_("Verify"))
1290         b.clicked.connect(do_verify)
1291         hbox.addWidget(b)
1292         b = QPushButton(_("Close"))
1293         b.clicked.connect(d.accept)
1294         hbox.addWidget(b)
1295         layout.addLayout(hbox, 4, 1)
1296         tab_widget.addTab(tab, "Verify")
1297
1298         vbox = QVBoxLayout()
1299         vbox.addWidget(tab_widget)
1300         d.setLayout(vbox)
1301         d.exec_()
1302
1303         
1304     def toggle_QR_window(self):
1305         if self.qr_window:
1306             self.qr_window.close()
1307             self.qr_window = None
1308         else:
1309             self.qr_window = QR_Window()
1310             item = self.receive_list.currentItem()
1311             if item:
1312                 address = str(item.text(1))
1313                 label = self.wallet.labels.get(address)
1314                 amount = self.wallet.requested_amounts.get(address)
1315                 self.qr_window.set_content( address, label, amount )
1316             self.qr_window.show()
1317
1318         self.qr_button.setText(self.qr_button_text())
1319         self.print_button.setHidden(self.qr_window is None)
1320         self.update_receive_tab()
1321
1322     def question(self, msg):
1323         return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1324
1325     def show_message(self, msg):
1326         QMessageBox.information(self, _('Message'), msg, _('OK'))
1327
1328     def password_dialog(self ):
1329         d = QDialog(self)
1330         d.setModal(1)
1331
1332         pw = QLineEdit()
1333         pw.setEchoMode(2)
1334
1335         vbox = QVBoxLayout()
1336         msg = _('Please enter your password')
1337         vbox.addWidget(QLabel(msg))
1338
1339         grid = QGridLayout()
1340         grid.setSpacing(8)
1341         grid.addWidget(QLabel(_('Password')), 1, 0)
1342         grid.addWidget(pw, 1, 1)
1343         vbox.addLayout(grid)
1344
1345         vbox.addLayout(ok_cancel_buttons(d))
1346         d.setLayout(vbox) 
1347
1348         if not d.exec_(): return
1349         return unicode(pw.text())
1350
1351
1352
1353
1354
1355     @staticmethod
1356     def change_password_dialog( wallet, parent=None ):
1357
1358         if not wallet.seed:
1359             QMessageBox.information(parent, _('Error'), _('No seed'), _('OK'))
1360             return
1361
1362         d = QDialog(parent)
1363         d.setModal(1)
1364
1365         pw = QLineEdit()
1366         pw.setEchoMode(2)
1367         new_pw = QLineEdit()
1368         new_pw.setEchoMode(2)
1369         conf_pw = QLineEdit()
1370         conf_pw.setEchoMode(2)
1371
1372         vbox = QVBoxLayout()
1373         if parent:
1374             msg = (_('Your wallet is encrypted. Use this dialog to change your password.')+'\n'\
1375                    +_('To disable wallet encryption, enter an empty new password.')) \
1376                    if wallet.use_encryption else _('Your wallet keys are not encrypted')
1377         else:
1378             msg = _("Please choose a password to encrypt your wallet keys.")+'\n'\
1379                   +_("Leave these fields empty if you want to disable encryption.")
1380         vbox.addWidget(QLabel(msg))
1381
1382         grid = QGridLayout()
1383         grid.setSpacing(8)
1384
1385         if wallet.use_encryption:
1386             grid.addWidget(QLabel(_('Password')), 1, 0)
1387             grid.addWidget(pw, 1, 1)
1388
1389         grid.addWidget(QLabel(_('New Password')), 2, 0)
1390         grid.addWidget(new_pw, 2, 1)
1391
1392         grid.addWidget(QLabel(_('Confirm Password')), 3, 0)
1393         grid.addWidget(conf_pw, 3, 1)
1394         vbox.addLayout(grid)
1395
1396         vbox.addLayout(ok_cancel_buttons(d))
1397         d.setLayout(vbox) 
1398
1399         if not d.exec_(): return
1400
1401         password = unicode(pw.text()) if wallet.use_encryption else None
1402         new_password = unicode(new_pw.text())
1403         new_password2 = unicode(conf_pw.text())
1404
1405         try:
1406             seed = wallet.pw_decode( wallet.seed, password)
1407         except:
1408             QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
1409             return
1410
1411         if new_password != new_password2:
1412             QMessageBox.warning(parent, _('Error'), _('Passwords do not match'), _('OK'))
1413             return
1414
1415         wallet.update_password(seed, password, new_password)
1416
1417     @staticmethod
1418     def seed_dialog(wallet, parent=None):
1419         d = QDialog(parent)
1420         d.setModal(1)
1421
1422         vbox = QVBoxLayout()
1423         msg = _("Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet.")
1424         vbox.addWidget(QLabel(msg))
1425
1426         grid = QGridLayout()
1427         grid.setSpacing(8)
1428
1429         seed_e = QLineEdit()
1430         grid.addWidget(QLabel(_('Seed or mnemonic')), 1, 0)
1431         grid.addWidget(seed_e, 1, 1)
1432
1433         gap_e = QLineEdit()
1434         gap_e.setText("5")
1435         grid.addWidget(QLabel(_('Gap limit')), 2, 0)
1436         grid.addWidget(gap_e, 2, 1)
1437         gap_e.textChanged.connect(lambda: numbify(gap_e,True))
1438         vbox.addLayout(grid)
1439
1440         vbox.addLayout(ok_cancel_buttons(d))
1441         d.setLayout(vbox) 
1442
1443         if not d.exec_(): return
1444
1445         try:
1446             gap = int(unicode(gap_e.text()))
1447         except:
1448             QMessageBox.warning(None, _('Error'), 'error', 'OK')
1449             sys.exit(0)
1450
1451         try:
1452             seed = unicode(seed_e.text())
1453             seed.decode('hex')
1454         except:
1455             print_error("Warning: Not hex, trying decode")
1456             try:
1457                 seed = mnemonic.mn_decode( seed.split(' ') )
1458             except:
1459                 QMessageBox.warning(None, _('Error'), _('I cannot decode this'), _('OK'))
1460                 sys.exit(0)
1461         if not seed:
1462             QMessageBox.warning(None, _('Error'), _('No seed'), 'OK')
1463             sys.exit(0)
1464         
1465         wallet.seed = str(seed)
1466         #print repr(wallet.seed)
1467         wallet.gap_limit = gap
1468         return True
1469
1470
1471
1472     def settings_dialog(self):
1473         d = QDialog(self)
1474         d.setModal(1)
1475         vbox = QVBoxLayout()
1476         msg = _('Here are the settings of your wallet.') + '\n'\
1477               + _('For more explanations, click on the help buttons next to each field.')
1478
1479         label = QLabel(msg)
1480         label.setFixedWidth(250)
1481         label.setWordWrap(True)
1482         label.setAlignment(Qt.AlignJustify)
1483         vbox.addWidget(label)
1484
1485         grid = QGridLayout()
1486         grid.setSpacing(8)
1487         vbox.addLayout(grid)
1488
1489         fee_label = QLabel(_('Transaction fee'))
1490         grid.addWidget(fee_label, 2, 0)
1491         fee_e = QLineEdit()
1492         fee_e.setText("%s"% str( Decimal( self.wallet.fee)/100000000 ) )
1493         grid.addWidget(fee_e, 2, 1)
1494         msg = _('Fee per transaction input. Transactions involving multiple inputs tend to require a higher fee.') + ' ' \
1495             + _('Recommended value') + ': 0.001'
1496         grid.addWidget(HelpButton(msg), 2, 2)
1497         fee_e.textChanged.connect(lambda: numbify(fee_e,False))
1498         if not self.config.is_modifiable('fee'):
1499             for w in [fee_e, fee_label]: w.setEnabled(False)
1500
1501         nz_label = QLabel(_('Display zeros'))
1502         grid.addWidget(nz_label, 3, 0)
1503         nz_e = QLineEdit()
1504         nz_e.setText("%d"% self.wallet.num_zeros)
1505         grid.addWidget(nz_e, 3, 1)
1506         msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1507         grid.addWidget(HelpButton(msg), 3, 2)
1508         nz_e.textChanged.connect(lambda: numbify(nz_e,True))
1509         if not self.config.is_modifiable('num_zeros'):
1510             for w in [nz_e, nz_label]: w.setEnabled(False)
1511
1512         usechange_cb = QCheckBox(_('Use change addresses'))
1513         grid.addWidget(usechange_cb, 5, 0)
1514         usechange_cb.setChecked(self.wallet.use_change)
1515         grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions. ')), 5, 2)
1516         if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
1517
1518         gap_label = QLabel(_('Gap limit'))
1519         grid.addWidget(gap_label, 6, 0)
1520         gap_e = QLineEdit()
1521         gap_e.setText("%d"% self.wallet.gap_limit)
1522         grid.addWidget(gap_e, 6, 1)
1523         msg =  _('The gap limit is the maximal number of contiguous unused addresses in your sequence of receiving addresses.') + '\n' \
1524               + _('You may increase it if you need more receiving addresses.') + '\n\n' \
1525               + _('Your current gap limit is') + ': %d'%self.wallet.gap_limit + '\n' \
1526               + _('Given the current status of your address sequence, the minimum gap limit you can use is: ') + '%d'%self.wallet.min_acceptable_gap() + '\n\n' \
1527               + _('Warning') + ': ' \
1528               + _('The gap limit parameter must be provided in order to recover your wallet from seed.') + ' ' \
1529               + _('Do not modify it if you do not understand what you are doing, or if you expect to recover your wallet without knowing it!') + '\n\n' 
1530         grid.addWidget(HelpButton(msg), 6, 2)
1531         gap_e.textChanged.connect(lambda: numbify(nz_e,True))
1532         if not self.config.is_modifiable('gap_limit'):
1533             for w in [gap_e, gap_label]: w.setEnabled(False)
1534         
1535         gui_label=QLabel(_('Default GUI') + ':')
1536         grid.addWidget(gui_label , 7, 0)
1537         gui_combo = QComboBox()
1538         gui_combo.addItems(['Lite', 'Classic', 'Gtk', 'Text'])
1539         index = gui_combo.findText(self.config.get("gui","classic").capitalize())
1540         if index==-1: index = 1
1541         gui_combo.setCurrentIndex(index)
1542         grid.addWidget(gui_combo, 7, 1)
1543         grid.addWidget(HelpButton(_('Select which GUI mode to use at start up. ')), 7, 2)
1544         if not self.config.is_modifiable('gui'):
1545             for w in [gui_combo, gui_label]: w.setEnabled(False)
1546
1547         vbox.addLayout(ok_cancel_buttons(d))
1548         d.setLayout(vbox) 
1549
1550         # run the dialog
1551         if not d.exec_(): return
1552
1553         fee = unicode(fee_e.text())
1554         try:
1555             fee = int( 100000000 * Decimal(fee) )
1556         except:
1557             QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
1558             return
1559
1560         if self.wallet.fee != fee:
1561             self.wallet.fee = fee
1562             self.wallet.save()
1563         
1564         nz = unicode(nz_e.text())
1565         try:
1566             nz = int( nz )
1567             if nz>8: nz=8
1568         except:
1569             QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
1570             return
1571
1572         if self.wallet.num_zeros != nz:
1573             self.wallet.num_zeros = nz
1574             self.config.set_key('num_zeros', nz, True)
1575             self.update_history_tab()
1576             self.update_receive_tab()
1577
1578         if self.wallet.use_change != usechange_cb.isChecked():
1579             self.wallet.use_change = usechange_cb.isChecked()
1580             self.config.set_key('use_change', self.wallet.use_change, True)
1581         
1582         try:
1583             n = int(gap_e.text())
1584         except:
1585             QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
1586             return
1587
1588         if self.wallet.gap_limit != n:
1589             r = self.wallet.change_gap_limit(n)
1590             if r:
1591                 self.update_receive_tab()
1592                 self.config.set_key('gap_limit', self.wallet.gap_limit, True)
1593             else:
1594                 QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
1595                     
1596         self.config.set_key("gui", str(gui_combo.currentText()).lower(), True)
1597
1598
1599
1600     @staticmethod 
1601     def network_dialog(wallet, parent=None):
1602         interface = wallet.interface
1603         if parent:
1604             if interface.is_connected:
1605                 status = _("Connected to")+" %s\n%d blocks"%(interface.host, wallet.verifier.height)
1606             else:
1607                 status = _("Not connected")
1608             server = interface.server
1609         else:
1610             import random
1611             status = _("Please choose a server.") + "\n" + _("Select 'Cancel' if you are offline.")
1612             server = interface.server
1613
1614         plist, servers_list = interface.get_servers_list()
1615
1616         d = QDialog(parent)
1617         d.setModal(1)
1618         d.setWindowTitle(_('Server'))
1619         d.setMinimumSize(375, 20)
1620
1621         vbox = QVBoxLayout()
1622         vbox.setSpacing(30)
1623
1624         hbox = QHBoxLayout()
1625         l = QLabel()
1626         l.setPixmap(QPixmap(":icons/network.png"))
1627         hbox.addStretch(10)
1628         hbox.addWidget(l)
1629         hbox.addWidget(QLabel(status))
1630         hbox.addStretch(50)
1631         vbox.addLayout(hbox)
1632
1633
1634         # grid layout
1635         grid = QGridLayout()
1636         grid.setSpacing(8)
1637         vbox.addLayout(grid)
1638
1639         # server
1640         server_protocol = QComboBox()
1641         server_host = QLineEdit()
1642         server_host.setFixedWidth(200)
1643         server_port = QLineEdit()
1644         server_port.setFixedWidth(60)
1645
1646         protocol_names = ['TCP', 'HTTP', 'TCP/SSL', 'HTTPS']
1647         protocol_letters = 'thsg'
1648         DEFAULT_PORTS = {'t':'50001', 's':'50002', 'h':'8081', 'g':'8082'}
1649         server_protocol.addItems(protocol_names)
1650
1651         grid.addWidget(QLabel(_('Server') + ':'), 0, 0)
1652         grid.addWidget(server_protocol, 0, 1)
1653         grid.addWidget(server_host, 0, 2)
1654         grid.addWidget(server_port, 0, 3)
1655
1656         def change_protocol(p):
1657             protocol = protocol_letters[p]
1658             host = unicode(server_host.text())
1659             pp = plist.get(host,DEFAULT_PORTS)
1660             if protocol not in pp.keys():
1661                 protocol = pp.keys()[0]
1662             port = pp[protocol]
1663             server_host.setText( host )
1664             server_port.setText( port )
1665
1666         server_protocol.connect(server_protocol, SIGNAL('currentIndexChanged(int)'), change_protocol)
1667         
1668         label = _('Active Servers') if wallet.interface.servers else _('Default Servers')
1669         servers_list_widget = QTreeWidget(parent)
1670         servers_list_widget.setHeaderLabels( [ label, _('Type') ] )
1671         servers_list_widget.setMaximumHeight(150)
1672         servers_list_widget.setColumnWidth(0, 240)
1673         for _host in servers_list.keys():
1674             _type = 'pruning' if servers_list[_host].get('pruning') else 'full'
1675             servers_list_widget.addTopLevelItem(QTreeWidgetItem( [ _host, _type ] ))
1676
1677         def change_server(host, protocol=None):
1678             pp = plist.get(host,DEFAULT_PORTS)
1679             if protocol:
1680                 port = pp.get(protocol)
1681                 if not port: protocol = None
1682                     
1683             if not protocol:
1684                 if 't' in pp.keys():
1685                     protocol = 't'
1686                     port = pp.get(protocol)
1687                 else:
1688                     protocol = pp.keys()[0]
1689                     port = pp.get(protocol)
1690             
1691             server_host.setText( host )
1692             server_port.setText( port )
1693             server_protocol.setCurrentIndex(protocol_letters.index(protocol))
1694
1695             if not plist: return
1696             for p in protocol_letters:
1697                 i = protocol_letters.index(p)
1698                 j = server_protocol.model().index(i,0)
1699                 if p not in pp.keys():
1700                     server_protocol.model().setData(j, QtCore.QVariant(0), QtCore.Qt.UserRole-1)
1701                 else:
1702                     server_protocol.model().setData(j, QtCore.QVariant(0,False), QtCore.Qt.UserRole-1)
1703
1704
1705         if server:
1706             host, port, protocol = server.split(':')
1707             change_server(host,protocol)
1708
1709         servers_list_widget.connect(servers_list_widget, SIGNAL('itemClicked(QTreeWidgetItem*, int)'), lambda x: change_server(unicode(x.text(0))))
1710         grid.addWidget(servers_list_widget, 1, 1, 1, 3)
1711
1712         if not wallet.config.is_modifiable('server'):
1713             for w in [server_host, server_port, server_protocol, servers_list_widget]: w.setEnabled(False)
1714
1715         # proxy setting
1716         proxy_mode = QComboBox()
1717         proxy_host = QLineEdit()
1718         proxy_host.setFixedWidth(200)
1719         proxy_port = QLineEdit()
1720         proxy_port.setFixedWidth(60)
1721         proxy_mode.addItems(['NONE', 'SOCKS4', 'SOCKS5', 'HTTP'])
1722
1723         def check_for_disable(index = False):
1724             if proxy_mode.currentText() != 'NONE':
1725                 proxy_host.setEnabled(True)
1726                 proxy_port.setEnabled(True)
1727             else:
1728                 proxy_host.setEnabled(False)
1729                 proxy_port.setEnabled(False)
1730
1731         check_for_disable()
1732         proxy_mode.connect(proxy_mode, SIGNAL('currentIndexChanged(int)'), check_for_disable)
1733
1734         if not wallet.config.is_modifiable('proxy'):
1735             for w in [proxy_host, proxy_port, proxy_mode]: w.setEnabled(False)
1736
1737         proxy_config = interface.proxy if interface.proxy else { "mode":"none", "host":"localhost", "port":"8080"}
1738         proxy_mode.setCurrentIndex(proxy_mode.findText(str(proxy_config.get("mode").upper())))
1739         proxy_host.setText(proxy_config.get("host"))
1740         proxy_port.setText(proxy_config.get("port"))
1741
1742         grid.addWidget(QLabel(_('Proxy') + ':'), 2, 0)
1743         grid.addWidget(proxy_mode, 2, 1)
1744         grid.addWidget(proxy_host, 2, 2)
1745         grid.addWidget(proxy_port, 2, 3)
1746
1747         # buttons
1748         vbox.addLayout(ok_cancel_buttons(d))
1749         d.setLayout(vbox) 
1750
1751         if not d.exec_(): return
1752
1753         server = unicode( server_host.text() ) + ':' + unicode( server_port.text() ) + ':' + (protocol_letters[server_protocol.currentIndex()])
1754         if proxy_mode.currentText() != 'NONE':
1755             proxy = { u'mode':unicode(proxy_mode.currentText()).lower(), u'host':unicode(proxy_host.text()), u'port':unicode(proxy_port.text()) }
1756         else:
1757             proxy = None
1758
1759         wallet.config.set_key("proxy", proxy, True)
1760         wallet.config.set_key("server", server, True)
1761         interface.set_server(server, proxy)
1762                 
1763         return True
1764
1765     def closeEvent(self, event):
1766         g = self.geometry()
1767         self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
1768         event.accept()
1769
1770
1771 class ElectrumGui:
1772
1773     def __init__(self, wallet, config, app=None):
1774         self.wallet = wallet
1775         self.config = config
1776         if app is None:
1777             self.app = QApplication(sys.argv)
1778
1779
1780     def restore_or_create(self):
1781         msg = _("Wallet file not found.")+"\n"+_("Do you want to create a new wallet, or to restore an existing one?")
1782         r = QMessageBox.question(None, _('Message'), msg, _('Create'), _('Restore'), _('Cancel'), 0, 2)
1783         if r==2: return None
1784         return 'restore' if r==1 else 'create'
1785
1786     def seed_dialog(self):
1787         return ElectrumWindow.seed_dialog( self.wallet )
1788
1789     def network_dialog(self):
1790         return ElectrumWindow.network_dialog( self.wallet, parent=None )
1791         
1792
1793     def show_seed(self):
1794         ElectrumWindow.show_seed_dialog(self.wallet)
1795
1796
1797     def password_dialog(self):
1798         ElectrumWindow.change_password_dialog(self.wallet)
1799
1800
1801     def restore_wallet(self):
1802         wallet = self.wallet
1803         # wait until we are connected, because the user might have selected another server
1804         if not wallet.interface.is_connected:
1805             waiting = lambda: False if wallet.interface.is_connected else "connecting...\n"
1806             waiting_dialog(waiting)
1807
1808         waiting = lambda: False if wallet.is_up_to_date() else "Please wait...\nAddresses generated: %d\nKilobytes received: %.1f"\
1809             %(len(wallet.all_addresses()), wallet.interface.bytes_received/1024.)
1810
1811         wallet.set_up_to_date(False)
1812         wallet.interface.poke('synchronizer')
1813         waiting_dialog(waiting)
1814         if wallet.is_found():
1815             print_error( "Recovery successful" )
1816         else:
1817             QMessageBox.information(None, _('Error'), _("No transactions found for this seed"), _('OK'))
1818
1819         return True
1820
1821     def main(self,url):
1822         s = Timer()
1823         s.start()
1824         w = ElectrumWindow(self.wallet, self.config)
1825         if url: w.set_url(url)
1826         w.app = self.app
1827         w.connect_slots(s)
1828         w.update_wallet()
1829         w.show()
1830
1831         self.app.exec_()