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