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