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