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