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