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