compatibility with 0.6 protocol
[electrum-nvc.git] / lib / gui_qt.py
1 #!/usr/bin/env python
2 #
3 # Electrum - lightweight Bitcoin client
4 # Copyright (C) 2012 thomasv@gitorious
5 #
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
19 import sys, time, datetime, re
20 from i18n import _
21 from util import print_error
22
23 try:
24     import PyQt4
25 except:
26     sys.exit("Error: Could not import PyQt4 on Linux systems, you may try 'sudo apt-get install python-qt4'")
27
28 from PyQt4.QtGui import *
29 from PyQt4.QtCore import *
30 import PyQt4.QtCore as QtCore
31 import PyQt4.QtGui as QtGui
32 from interface import DEFAULT_SERVERS
33
34 try:
35     import icons_rc
36 except:
37     sys.exit("Error: Could not import icons_rc.py, please generate it with: 'pyrcc4 icons.qrc -o lib/icons_rc.py'")
38
39 from wallet import format_satoshis
40 import bmp, mnemonic, pyqrnative, qrscanner
41
42 from decimal import Decimal
43
44 import platform
45
46 if platform.system() == 'Windows':
47     MONOSPACE_FONT = 'Lucida Console'
48 elif platform.system() == 'Darwin':
49     MONOSPACE_FONT = 'Monaco'
50 else:
51     MONOSPACE_FONT = 'monospace'
52
53 ALIAS_REGEXP = '^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$'    
54
55 def numbify(entry, is_int = False):
56     text = unicode(entry.text()).strip()
57     pos = entry.cursorPosition()
58     chars = '0123456789'
59     if not is_int: chars +='.'
60     s = ''.join([i for i in text if i in chars])
61     if not is_int:
62         if '.' in s:
63             p = s.find('.')
64             s = s.replace('.','')
65             s = s[:p] + '.' + s[p:p+8]
66         try:
67             amount = int( Decimal(s) * 100000000 )
68         except:
69             amount = None
70     else:
71         try:
72             amount = int( s )
73         except:
74             amount = None
75     entry.setText(s)
76     entry.setCursorPosition(pos)
77     return amount
78
79
80 class Timer(QtCore.QThread):
81     def run(self):
82         while True:
83             self.emit(QtCore.SIGNAL('timersignal'))
84             time.sleep(0.5)
85
86 class HelpButton(QPushButton):
87     def __init__(self, text):
88         QPushButton.__init__(self, '?')
89         self.setFocusPolicy(Qt.NoFocus)
90         self.setFixedWidth(20)
91         self.clicked.connect(lambda: QMessageBox.information(self, 'Help', text, 'OK') )
92
93
94 class EnterButton(QPushButton):
95     def __init__(self, text, func):
96         QPushButton.__init__(self, text)
97         self.func = func
98         self.clicked.connect(func)
99
100     def keyPressEvent(self, e):
101         if e.key() == QtCore.Qt.Key_Return:
102             apply(self.func,())
103
104 class MyTreeWidget(QTreeWidget):
105     def __init__(self, parent):
106         QTreeWidget.__init__(self, parent)
107         def ddfr(item):
108             if not item: return
109             for i in range(0,self.viewport().height()/5):
110                 if self.itemAt(QPoint(0,i*5)) == item:
111                     break
112             else:
113                 return
114             for j in range(0,30):
115                 if self.itemAt(QPoint(0,i*5 + j)) != item:
116                     break
117             self.emit(SIGNAL('customContextMenuRequested(const QPoint&)'), QPoint(50, i*5 + j - 1))
118
119         self.connect(self, SIGNAL('itemActivated(QTreeWidgetItem*, int)'), ddfr)
120         
121
122
123
124 class StatusBarButton(QPushButton):
125     def __init__(self, icon, tooltip, func):
126         QPushButton.__init__(self, icon, '')
127         self.setToolTip(tooltip)
128         self.setFlat(True)
129         self.setMaximumWidth(25)
130         self.clicked.connect(func)
131         self.func = func
132
133     def keyPressEvent(self, e):
134         if e.key() == QtCore.Qt.Key_Return:
135             apply(self.func,())
136
137
138 class QRCodeWidget(QWidget):
139
140     def __init__(self, addr):
141         super(QRCodeWidget, self).__init__()
142         self.setGeometry(300, 300, 350, 350)
143         self.set_addr(addr)
144
145     def set_addr(self, addr):
146         self.addr = addr
147         self.qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.L)
148         self.qr.addData(addr)
149         self.qr.make()
150         
151     def paintEvent(self, e):
152         qp = QtGui.QPainter()
153         qp.begin(self)
154         boxsize = 6
155         size = self.qr.getModuleCount()*boxsize
156         k = self.qr.getModuleCount()
157         black = QColor(0, 0, 0, 255)
158         white = QColor(255, 255, 255, 255)
159         for r in range(k):
160             for c in range(k):
161                 if self.qr.isDark(r, c):
162                     qp.setBrush(black)
163                     qp.setPen(black)
164                 else:
165                     qp.setBrush(white)
166                     qp.setPen(white)
167                 qp.drawRect(c*boxsize, r*boxsize, boxsize, boxsize)
168         qp.end()
169         
170
171 def waiting_dialog(f):
172
173     s = Timer()
174     s.start()
175     w = QDialog()
176     w.resize(200, 70)
177     w.setWindowTitle('Electrum')
178     l = QLabel('')
179     vbox = QVBoxLayout()
180     vbox.addWidget(l)
181     w.setLayout(vbox)
182     w.show()
183     def ff():
184         s = f()
185         if s: l.setText(s)
186         else: w.close()
187     w.connect(s, QtCore.SIGNAL('timersignal'), ff)
188     w.exec_()
189     w.destroy()
190
191
192 def ok_cancel_buttons(dialog):
193     hbox = QHBoxLayout()
194     hbox.addStretch(1)
195     b = QPushButton("OK")
196     hbox.addWidget(b)
197     b.clicked.connect(dialog.accept)
198     b = QPushButton("Cancel")
199     hbox.addWidget(b)
200     b.clicked.connect(dialog.reject)
201     return hbox
202
203
204 class ElectrumWindow(QMainWindow):
205
206     def __init__(self, wallet, config):
207         QMainWindow.__init__(self)
208         self.wallet = wallet
209         self.config = config
210         self.wallet.interface.register_callback('updated', self.update_callback)
211         self.wallet.interface.register_callback('connected', self.update_callback)
212         self.wallet.interface.register_callback('disconnected', self.update_callback)
213         self.wallet.interface.register_callback('disconnecting', self.update_callback)
214
215         self.detailed_view = config.get('qt_detailed_view', False)
216
217         self.funds_error = False
218         self.completions = QStringListModel()
219
220         self.tabs = tabs = QTabWidget(self)
221         tabs.addTab(self.create_history_tab(), _('History') )
222         if self.wallet.seed:
223             tabs.addTab(self.create_send_tab(), _('Send') )
224         tabs.addTab(self.create_receive_tab(), _('Receive') )
225         tabs.addTab(self.create_contacts_tab(), _('Contacts') )
226         tabs.addTab(self.create_wall_tab(), _('Wall') )
227         tabs.setMinimumSize(600, 400)
228         tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
229         self.setCentralWidget(tabs)
230         self.create_status_bar()
231
232         g = self.config.get("winpos-qt",[100, 100, 840, 400])
233         self.setGeometry(g[0], g[1], g[2], g[3])
234         title = 'Electrum ' + self.wallet.electrum_version + '  -  ' + self.config.path
235         if not self.wallet.seed: title += ' [seedless]'
236         self.setWindowTitle( title )
237
238         QShortcut(QKeySequence("Ctrl+W"), self, self.close)
239         QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
240         QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
241         QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
242         
243         self.connect(self, QtCore.SIGNAL('updatesignal'), self.update_wallet)
244         self.history_list.setFocus(True)
245
246         # dark magic fix by flatfly; https://bitcointalk.org/index.php?topic=73651.msg959913#msg959913
247         if platform.system() == 'Windows':
248             n = 3 if self.wallet.seed else 2
249             tabs.setCurrentIndex (n)
250             tabs.setCurrentIndex (0)
251
252
253     def connect_slots(self, sender):
254         if self.wallet.seed:
255             self.connect(sender, QtCore.SIGNAL('timersignal'), self.check_recipient)
256             self.previous_payto_e=''
257
258     def check_recipient(self):
259         if self.payto_e.hasFocus():
260             return
261         r = unicode( self.payto_e.text() )
262         if r != self.previous_payto_e:
263             self.previous_payto_e = r
264             r = r.strip()
265             if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r):
266                 try:
267                     to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
268                 except:
269                     return
270                 if to_address:
271                     s = r + '  <' + to_address + '>'
272                     self.payto_e.setText(s)
273
274
275     def update_callback(self):
276         self.emit(QtCore.SIGNAL('updatesignal'))
277
278     def update_wallet(self):
279         if self.wallet.interface and self.wallet.interface.is_connected:
280             if not self.wallet.up_to_date:
281                 text = _( "Synchronizing..." )
282                 icon = QIcon(":icons/status_waiting.png")
283             else:
284                 c, u = self.wallet.get_balance()
285                 text =  _( "Balance" ) + ": %s "%( format_satoshis(c,False,self.wallet.num_zeros) )
286                 if u: text +=  "[%s unconfirmed]"%( format_satoshis(u,True,self.wallet.num_zeros).strip() )
287                 icon = QIcon(":icons/status_connected.png")
288         else:
289             text = _( "Not connected" )
290             icon = QIcon(":icons/status_disconnected.png")
291
292         if self.funds_error:
293             text = _( "Not enough funds" )
294
295         self.statusBar().showMessage(text)
296         self.status_button.setIcon( icon )
297
298         if self.wallet.up_to_date:
299             self.textbox.setText( self.wallet.banner )
300             self.update_history_tab()
301             self.update_receive_tab()
302             self.update_contacts_tab()
303             self.update_completions()
304
305
306     def create_history_tab(self):
307         self.history_list = l = MyTreeWidget(self)
308         l.setColumnCount(5)
309         l.setColumnWidth(0, 40) 
310         l.setColumnWidth(1, 140) 
311         l.setColumnWidth(2, 350) 
312         l.setColumnWidth(3, 140) 
313         l.setColumnWidth(4, 140) 
314         l.setHeaderLabels( [ '', _( 'Date' ), _( 'Description' ) , _('Amount'), _('Balance')] )
315         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
316         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
317         l.setContextMenuPolicy(Qt.CustomContextMenu)
318         l.customContextMenuRequested.connect(self.create_history_menu)
319         return l
320
321     def create_history_menu(self, position):
322         self.history_list.selectedIndexes() 
323         item = self.history_list.currentItem()
324         if not item: return
325         tx_hash = str(item.toolTip(0))
326         menu = QMenu()
327         menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
328         menu.addAction(_("Details"), lambda: self.tx_details(tx_hash))
329         menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
330         menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
331
332     def tx_details(self, tx_hash):
333         tx = self.wallet.transactions.get(tx_hash)
334
335         conf = self.wallet.verifier.get_confirmations(tx_hash)
336         if conf:
337             time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3]
338         else:
339             time_str = 'pending'
340
341         inputs = map(lambda x: x.get('address'), tx['inputs'])
342         outputs = map(lambda x: x.get('address'), tx['outputs'])
343         tx_details = _("Transaction Details") +"\n\n" \
344             + "Transaction ID:\n" + tx_hash + "\n\n" \
345             + "Status: %d confirmations\n\n"%conf  \
346             + "Date: %s\n\n"%time_str \
347             + "Inputs:\n-"+ '\n-'.join(inputs) + "\n\n" \
348             + "Outputs:\n-"+ '\n-'.join(outputs)
349
350         r = self.wallet.receipts.get(tx_hash)
351         if r:
352             tx_details += "\n_______________________________________" \
353                 + '\n\nSigned URI: ' + r[2] \
354                 + "\n\nSigned by: " + r[0] \
355                 + '\n\nSignature: ' + r[1]
356
357         QMessageBox.information(self, 'Details', tx_details, 'OK')
358
359
360     def tx_label_clicked(self, item, column):
361         if column==2 and item.isSelected():
362             tx_hash = str(item.toolTip(0))
363             self.is_edit=True
364             #if not self.wallet.labels.get(tx_hash): item.setText(2,'')
365             item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
366             self.history_list.editItem( item, column )
367             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
368             self.is_edit=False
369
370     def tx_label_changed(self, item, column):
371         if self.is_edit: 
372             return
373         self.is_edit=True
374         tx_hash = str(item.toolTip(0))
375         tx = self.wallet.transactions.get(tx_hash)
376         s = self.wallet.labels.get(tx_hash)
377         text = unicode( item.text(2) )
378         if text: 
379             self.wallet.labels[tx_hash] = text
380             item.setForeground(2, QBrush(QColor('black')))
381         else:
382             if s: self.wallet.labels.pop(tx_hash)
383             text = self.wallet.get_default_label(tx_hash)
384             item.setText(2, text)
385             item.setForeground(2, QBrush(QColor('gray')))
386         self.is_edit=False
387
388     def edit_label(self, is_recv):
389         l = self.receive_list if is_recv else self.contacts_list
390         c = 2 if is_recv else 1
391         item = l.currentItem()
392         item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
393         l.editItem( item, c )
394         item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
395
396     def address_label_clicked(self, item, column, l, column_addr, column_label):
397         if column==column_label and item.isSelected():
398             addr = unicode( item.text(column_addr) )
399             label = unicode( item.text(column_label) )
400             if label in self.wallet.aliases.keys():
401                 return
402             item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
403             l.editItem( item, column )
404             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
405
406     def address_label_changed(self, item, column, l, column_addr, column_label):
407         addr = unicode( item.text(column_addr) )
408         text = unicode( item.text(column_label) )
409         changed = False
410
411         if text:
412             if text not in self.wallet.aliases.keys():
413                 old_addr = self.wallet.labels.get(text)
414                 if old_addr != addr:
415                     self.wallet.labels[addr] = text
416                     changed = True
417             else:
418                 print_error("Error: This is one of your aliases")
419                 label = self.wallet.labels.get(addr,'')
420                 item.setText(column_label, QString(label))
421         else:
422             s = self.wallet.labels.get(addr)
423             if s: 
424                 self.wallet.labels.pop(addr)
425                 changed = True
426
427         if changed:
428             self.wallet.update_tx_labels()
429             self.update_history_tab()
430             self.update_completions()
431
432
433     def update_history_tab(self):
434         self.history_list.clear()
435         balance = 0
436         for tx in self.wallet.get_tx_history():
437             tx_hash = tx['tx_hash']
438             conf = self.wallet.verifier.get_confirmations(tx_hash)
439             if conf:
440                 time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3]
441                 if conf == 0:
442                     icon = QIcon(":icons/unconfirmed.png")
443                 elif conf < 6:
444                     icon = QIcon(":icons/clock%d.png"%conf)
445                 else:
446                     icon = QIcon(":icons/confirmed.png")
447             else:
448                 time_str = 'pending'
449                 icon = QIcon(":icons/unconfirmed.png")
450             v = self.wallet.get_tx_value(tx_hash)
451             balance += v 
452             label, is_default_label = self.wallet.get_label(tx_hash)
453
454             item = QTreeWidgetItem( [ '', time_str, label, format_satoshis(v,True,self.wallet.num_zeros), format_satoshis(balance,False,self.wallet.num_zeros)] )
455             item.setFont(2, QFont(MONOSPACE_FONT))
456             item.setFont(3, QFont(MONOSPACE_FONT))
457             item.setFont(4, QFont(MONOSPACE_FONT))
458             item.setToolTip(0, tx_hash)
459             if is_default_label:
460                 item.setForeground(2, QBrush(QColor('grey')))
461
462             item.setIcon(0, icon)
463             self.history_list.insertTopLevelItem(0,item)
464
465         self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
466
467
468     def create_send_tab(self):
469         w = QWidget()
470
471         grid = QGridLayout()
472         grid.setSpacing(8)
473         grid.setColumnMinimumWidth(3,300)
474         grid.setColumnStretch(5,1)
475
476         self.payto_e = QLineEdit()
477         grid.addWidget(QLabel(_('Pay to')), 1, 0)
478         grid.addWidget(self.payto_e, 1, 1, 1, 3)
479         
480         def fill_from_qr():
481             qrcode = qrscanner.scan_qr()
482             if 'address' in qrcode:
483                 self.payto_e.setText(qrcode['address'])
484             if 'amount' in qrcode:
485                 self.amount_e.setText(str(qrcode['amount']))
486             if 'label' in qrcode:
487                 self.message_e.setText(qrcode['label'])
488             if 'message' in qrcode:
489                 self.message_e.setText("%s (%s)" % (self.message_e.text(), qrcode['message']))
490                 
491
492         if qrscanner.is_available():
493             b = QPushButton(_("Scan QR code"))
494             b.clicked.connect(fill_from_qr)
495             grid.addWidget(b, 1, 5)
496     
497         grid.addWidget(HelpButton(_('Recipient of the funds.') + '\n\n' + _('You may enter a Bitcoin address, a label from your list of contacts (a list of completions will be proposed), or an alias (email-like address that forwards to a Bitcoin address)')), 1, 4)
498
499         completer = QCompleter()
500         completer.setCaseSensitivity(False)
501         self.payto_e.setCompleter(completer)
502         completer.setModel(self.completions)
503
504         self.message_e = QLineEdit()
505         grid.addWidget(QLabel(_('Description')), 2, 0)
506         grid.addWidget(self.message_e, 2, 1, 1, 3)
507         grid.addWidget(HelpButton(_('Description of the transaction (not mandatory).') + '\n\n' + _('The description is not sent to the recipient of the funds. It is stored in your wallet file, and displayed in the \'History\' tab.')), 2, 4)
508
509         self.amount_e = QLineEdit()
510         grid.addWidget(QLabel(_('Amount')), 3, 0)
511         grid.addWidget(self.amount_e, 3, 1, 1, 2)
512         grid.addWidget(HelpButton(
513                 _('Amount to be sent.') + '\n\n' \
514                     + _('The amount will be displayed in red if you do not have enough funds in your wallet. Note that if you have frozen some of your addresses, the available funds will be lower than your total balance.')), 3, 3)
515         
516         self.fee_e = QLineEdit()
517         grid.addWidget(QLabel(_('Fee')), 4, 0)
518         grid.addWidget(self.fee_e, 4, 1, 1, 2) 
519         grid.addWidget(HelpButton(
520                 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
521                     + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
522                     + _('A suggested fee is automatically added to this field. You may override it. The suggested fee increases with the size of the transaction.')), 4, 3)
523         
524         b = EnterButton(_("Send"), self.do_send)
525         grid.addWidget(b, 6, 1)
526
527         b = EnterButton(_("Clear"),self.do_clear)
528         grid.addWidget(b, 6, 2)
529
530         self.payto_sig = QLabel('')
531         grid.addWidget(self.payto_sig, 7, 0, 1, 4)
532
533         QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
534         QShortcut(QKeySequence("Down"), w, w.focusNextChild)
535         w.setLayout(grid) 
536
537         w2 = QWidget()
538         vbox = QVBoxLayout()
539         vbox.addWidget(w)
540         vbox.addStretch(1)
541         w2.setLayout(vbox)
542
543         def entry_changed( is_fee ):
544             self.funds_error = False
545             amount = numbify(self.amount_e)
546             fee = numbify(self.fee_e)
547             if not is_fee: fee = None
548             if amount is None:
549                 return
550             inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
551             if not is_fee:
552                 self.fee_e.setText( str( Decimal( fee ) / 100000000 ) )
553             if inputs:
554                 palette = QPalette()
555                 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
556             else:
557                 palette = QPalette()
558                 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
559                 self.funds_error = True
560             self.amount_e.setPalette(palette)
561             self.fee_e.setPalette(palette)
562
563         self.amount_e.textChanged.connect(lambda: entry_changed(False) )
564         self.fee_e.textChanged.connect(lambda: entry_changed(True) )
565
566         return w2
567
568
569     def update_completions(self):
570         l = []
571         for addr,label in self.wallet.labels.items():
572             if addr in self.wallet.addressbook:
573                 l.append( label + '  <' + addr + '>')
574         l = l + self.wallet.aliases.keys()
575
576         self.completions.setStringList(l)
577
578
579
580     def do_send(self):
581
582         label = unicode( self.message_e.text() )
583         r = unicode( self.payto_e.text() )
584         r = r.strip()
585
586         # alias
587         m1 = re.match(ALIAS_REGEXP, r)
588         # label or alias, with address in brackets
589         m2 = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
590         
591         if m1:
592             to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
593             if not to_address:
594                 return
595         elif m2:
596             to_address = m2.group(2)
597         else:
598             to_address = r
599
600         if not self.wallet.is_valid(to_address):
601             QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
602             return
603
604         try:
605             amount = int( Decimal( unicode( self.amount_e.text())) * 100000000 )
606         except:
607             QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
608             return
609         try:
610             fee = int( Decimal( unicode( self.fee_e.text())) * 100000000 )
611         except:
612             QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
613             return
614
615         if self.wallet.use_encryption:
616             password = self.password_dialog()
617             if not password:
618                 return
619         else:
620             password = None
621
622         try:
623             tx = self.wallet.mktx( to_address, amount, label, password, fee)
624         except BaseException, e:
625             self.show_message(str(e))
626             return
627             
628         h = self.wallet.send_tx(tx)
629         waiting_dialog(lambda: False if self.wallet.tx_event.isSet() else _("Please wait..."))
630         status, msg = self.wallet.receive_tx( h )
631
632         if status:
633             QMessageBox.information(self, '', _('Payment sent.')+'\n'+msg, _('OK'))
634             self.do_clear()
635             self.update_contacts_tab()
636         else:
637             QMessageBox.warning(self, _('Error'), msg, _('OK'))
638
639
640     def set_url(self, url):
641         payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question)
642         self.tabs.setCurrentIndex(1)
643         label = self.wallet.labels.get(payto)
644         m_addr = label + '  <'+ payto+'>' if label else payto
645         self.payto_e.setText(m_addr)
646
647         self.message_e.setText(message)
648         self.amount_e.setText(amount)
649         if identity:
650             self.set_frozen(self.payto_e,True)
651             self.set_frozen(self.amount_e,True)
652             self.set_frozen(self.message_e,True)
653             self.payto_sig.setText( '      The bitcoin URI was signed by ' + identity )
654         else:
655             self.payto_sig.setVisible(False)
656
657     def do_clear(self):
658         self.payto_sig.setVisible(False)
659         for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
660             e.setText('')
661             self.set_frozen(e,False)
662
663     def set_frozen(self,entry,frozen):
664         if frozen:
665             entry.setReadOnly(True)
666             entry.setFrame(False)
667             palette = QPalette()
668             palette.setColor(entry.backgroundRole(), QColor('lightgray'))
669             entry.setPalette(palette)
670         else:
671             entry.setReadOnly(False)
672             entry.setFrame(True)
673             palette = QPalette()
674             palette.setColor(entry.backgroundRole(), QColor('white'))
675             entry.setPalette(palette)
676
677
678     def toggle_freeze(self,addr):
679         if not addr: return
680         if addr in self.wallet.frozen_addresses:
681             self.wallet.unfreeze(addr)
682         else:
683             self.wallet.freeze(addr)
684         self.update_receive_tab()
685
686     def toggle_priority(self,addr):
687         if not addr: return
688         if addr in self.wallet.prioritized_addresses:
689             self.wallet.unprioritize(addr)
690         else:
691             self.wallet.prioritize(addr)
692         self.update_receive_tab()
693
694
695     def create_list_tab(self, headers):
696         "generic tab creation method"
697         l = MyTreeWidget(self)
698         l.setColumnCount( len(headers) )
699         l.setHeaderLabels( headers )
700
701         w = QWidget()
702         vbox = QVBoxLayout()
703         w.setLayout(vbox)
704
705         vbox.setMargin(0)
706         vbox.setSpacing(0)
707         vbox.addWidget(l)
708         buttons = QWidget()
709         vbox.addWidget(buttons)
710
711         hbox = QHBoxLayout()
712         hbox.setMargin(0)
713         hbox.setSpacing(0)
714         buttons.setLayout(hbox)
715
716         return l,w,hbox
717
718
719     def create_receive_tab(self):
720         l,w,hbox = self.create_list_tab([_('Flags'), _('Address'), _('Label'), _('Balance'), _('Tx')])
721         l.setContextMenuPolicy(Qt.CustomContextMenu)
722         l.customContextMenuRequested.connect(self.create_receive_menu)
723         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,1,2))
724         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,1,2))
725         self.receive_list = l
726         self.receive_buttons_hbox = hbox
727         self.details_button = EnterButton(self.details_button_text(), self.toggle_detailed_view)
728         hbox.addWidget(self.details_button)
729         hbox.addStretch(1)
730         return w
731
732     def details_button_text(self):
733         return _('Hide details') if self.detailed_view else _('Show details')
734
735     def toggle_detailed_view(self):
736         self.detailed_view = not self.detailed_view
737         self.config.set_key('qt_detailed_view', self.detailed_view, True)
738
739         self.details_button.setText(self.details_button_text())
740         self.wallet.save()
741         self.update_receive_tab()
742         self.update_contacts_tab()
743
744
745     def create_contacts_tab(self):
746         l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
747         l.setContextMenuPolicy(Qt.CustomContextMenu)
748         l.customContextMenuRequested.connect(self.create_contact_menu)
749         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
750         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
751         self.contacts_list = l
752         self.contacts_buttons_hbox = hbox
753         hbox.addWidget(EnterButton(_("New"), self.new_contact_dialog))
754         hbox.addStretch(1)
755         return w
756
757
758     def create_receive_menu(self, position):
759         # fixme: this function apparently has a side effect.
760         # if it is not called the menu pops up several times
761         #self.receive_list.selectedIndexes() 
762
763         item = self.receive_list.itemAt(position)
764         if not item: return
765         addr = unicode(item.text(1))
766         menu = QMenu()
767         menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
768         menu.addAction(_("View QR code"),lambda: self.show_address_qrcode(addr))
769         menu.addAction(_("Edit label"), lambda: self.edit_label(True))
770
771         t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
772         menu.addAction(t, lambda: self.toggle_freeze(addr))
773         t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
774         menu.addAction(t, lambda: self.toggle_priority(addr))
775         menu.exec_(self.receive_list.viewport().mapToGlobal(position))
776
777
778     def payto(self, x, is_alias):
779         if not x: return
780         if is_alias:
781             label = x
782             m_addr = label
783         else:
784             addr = x
785             label = self.wallet.labels.get(addr)
786             m_addr = label + '  <' + addr + '>' if label else addr
787         self.tabs.setCurrentIndex(1)
788         self.payto_e.setText(m_addr)
789         self.amount_e.setFocus()
790
791     def delete_contact(self, x, is_alias):
792         if self.question("Do you want to remove %s from your list of contacts?"%x):
793             if not is_alias and x in self.wallet.addressbook:
794                 self.wallet.addressbook.remove(x)
795                 if x in self.wallet.labels.keys():
796                     self.wallet.labels.pop(x)
797             elif is_alias and x in self.wallet.aliases:
798                 self.wallet.aliases.pop(x)
799             self.update_history_tab()
800             self.update_contacts_tab()
801             self.update_completions()
802
803     def create_contact_menu(self, position):
804         # fixme: this function apparently has a side effect.
805         # if it is not called the menu pops up several times
806         #self.contacts_list.selectedIndexes() 
807
808         item = self.contacts_list.itemAt(position)
809         if not item: return
810         addr = unicode(item.text(0))
811         label = unicode(item.text(1))
812         is_alias = label in self.wallet.aliases.keys()
813         x = label if is_alias else addr
814         menu = QMenu()
815         menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
816         menu.addAction(_("Pay to"), lambda: self.payto(x, is_alias))
817         menu.addAction(_("View QR code"),lambda: self.show_address_qrcode(addr))
818         if not is_alias:
819             menu.addAction(_("Edit label"), lambda: self.edit_label(False))
820         else:
821             menu.addAction(_("View alias details"), lambda: self.show_contact_details(label))
822         menu.addAction(_("Delete"), lambda: self.delete_contact(x,is_alias))
823         menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
824
825
826     def update_receive_tab(self):
827         l = self.receive_list
828         l.clear()
829         l.setColumnHidden(0,not self.detailed_view)
830         l.setColumnHidden(3,not self.detailed_view)
831         l.setColumnHidden(4,not self.detailed_view)
832         l.setColumnWidth(0, 50) 
833         l.setColumnWidth(1, 310) 
834         l.setColumnWidth(2, 250)
835         l.setColumnWidth(3, 130) 
836         l.setColumnWidth(4, 10)
837
838         gap = 0
839         is_red = False
840         for address in self.wallet.all_addresses():
841
842             if self.wallet.is_change(address) and not self.detailed_view:
843                 continue
844
845             label = self.wallet.labels.get(address,'')
846             n = 0 
847             h = self.wallet.history.get(address,[])
848             if h == ['*']: h = []
849
850             for tx_hash, tx_height in h:
851                 tx = self.wallet.transactions.get(tx_hash)
852                 if tx: 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.transactions.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("Electrum")
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         index = gui_combo.findText(self.config.get("gui","classic").capitalize())
1304         if index==-1: index = 1
1305         gui_combo.setCurrentIndex(index)
1306         grid.addWidget(gui_combo, 7, 1)
1307         grid.addWidget(HelpButton(_('Select which GUI mode to use at start up. ')), 7, 2)
1308         if not self.config.is_modifiable('gui'):
1309             for w in [gui_combo, gui_label]: w.setEnabled(False)
1310
1311         vbox.addLayout(ok_cancel_buttons(d))
1312         d.setLayout(vbox) 
1313
1314         # run the dialog
1315         if not d.exec_(): return
1316
1317         fee = unicode(fee_e.text())
1318         try:
1319             fee = int( 100000000 * Decimal(fee) )
1320         except:
1321             QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
1322             return
1323
1324         if self.wallet.fee != fee:
1325             self.wallet.fee = fee
1326             self.wallet.save()
1327         
1328         nz = unicode(nz_e.text())
1329         try:
1330             nz = int( nz )
1331             if nz>8: nz=8
1332         except:
1333             QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
1334             return
1335
1336         if self.wallet.num_zeros != nz:
1337             self.wallet.num_zeros = nz
1338             self.config.set_key('num_zeros', nz, True)
1339             self.update_history_tab()
1340             self.update_receive_tab()
1341
1342         if self.wallet.use_change != usechange_cb.isChecked():
1343             self.wallet.use_change = usechange_cb.isChecked()
1344             self.config.set_key('use_change', self.wallet.use_change, True)
1345         
1346         try:
1347             n = int(gap_e.text())
1348         except:
1349             QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
1350             return
1351
1352         if self.wallet.gap_limit != n:
1353             r = self.wallet.change_gap_limit(n)
1354             if r:
1355                 self.update_receive_tab()
1356                 self.config.set_key('gap_limit', self.wallet.gap_limit, True)
1357             else:
1358                 QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
1359                     
1360         self.config.set_key("gui", str(gui_combo.currentText()).lower(), True)
1361
1362
1363
1364     @staticmethod 
1365     def network_dialog(wallet, parent=None):
1366         interface = wallet.interface
1367         if parent:
1368             if interface.is_connected:
1369                 status = _("Connected to")+" %s\n%d blocks"%(interface.host, wallet.verifier.height)
1370             else:
1371                 status = _("Not connected")
1372         else:
1373             import random
1374             status = _("Please choose a server.")
1375
1376         server = interface.server
1377
1378         plist = {}
1379         if not wallet.interface.servers:
1380             servers_list = []
1381             for x in DEFAULT_SERVERS:
1382                 h,port,protocol = x.split(':')
1383                 servers_list.append( (h,[(protocol,port)] ) )
1384         else:
1385             servers_list = wallet.interface.servers
1386             for item in servers_list:
1387                 _host, pp = item
1388                 z = {}
1389                 for item2 in pp:
1390                     _protocol, _port = item2
1391                     z[_protocol] = _port
1392                 plist[_host] = z
1393
1394         d = QDialog(parent)
1395         d.setModal(1)
1396         d.setWindowTitle(_('Server'))
1397         d.setMinimumSize(375, 20)
1398
1399         vbox = QVBoxLayout()
1400         vbox.setSpacing(30)
1401
1402         hbox = QHBoxLayout()
1403         l = QLabel()
1404         l.setPixmap(QPixmap(":icons/network.png"))
1405         hbox.addStretch(10)
1406         hbox.addWidget(l)
1407         hbox.addWidget(QLabel(status))
1408         hbox.addStretch(50)
1409         vbox.addLayout(hbox)
1410
1411
1412         # grid layout
1413         grid = QGridLayout()
1414         grid.setSpacing(8)
1415         vbox.addLayout(grid)
1416
1417         # server
1418         server_protocol = QComboBox()
1419         server_host = QLineEdit()
1420         server_host.setFixedWidth(200)
1421         server_port = QLineEdit()
1422         server_port.setFixedWidth(60)
1423
1424         protocol_names = ['TCP', 'HTTP', 'TCP/SSL', 'HTTPS']
1425         protocol_letters = 'thsg'
1426         DEFAULT_PORTS = {'t':'50001', 's':'50002', 'h':'8081', 'g':'8082'}
1427         server_protocol.addItems(protocol_names)
1428
1429         grid.addWidget(QLabel(_('Server') + ':'), 0, 0)
1430         grid.addWidget(server_protocol, 0, 1)
1431         grid.addWidget(server_host, 0, 2)
1432         grid.addWidget(server_port, 0, 3)
1433
1434         host, port, protocol = server.split(':')
1435
1436         def change_protocol(p):
1437             protocol = protocol_letters[p]
1438             host = unicode(server_host.text())
1439             pp = plist.get(host,DEFAULT_PORTS)
1440             if protocol not in pp.keys():
1441                 protocol = pp.keys()[0]
1442             port = pp[protocol]
1443             server_host.setText( host )
1444             server_port.setText( port )
1445
1446         server_protocol.connect(server_protocol, SIGNAL('currentIndexChanged(int)'), change_protocol)
1447         
1448         label = _('Active Servers') if wallet.interface.servers else _('Default Servers')
1449         servers_list_widget = QTreeWidget(parent)
1450         servers_list_widget.setHeaderLabels( [ label ] )
1451         servers_list_widget.setMaximumHeight(150)
1452         for _host, _x in servers_list:
1453             servers_list_widget.addTopLevelItem(QTreeWidgetItem( [ _host ] ))
1454
1455
1456         def change_server(host, protocol=None):
1457             pp = plist.get(host,DEFAULT_PORTS)
1458             if protocol:
1459                 port = pp.get(protocol)
1460                 if not port: protocol = None
1461                     
1462             if not protocol:
1463                 if 't' in pp.keys():
1464                     protocol = 't'
1465                     port = pp.get(protocol)
1466                 else:
1467                     protocol = pp.keys()[0]
1468                     port = pp.get(protocol)
1469             
1470             server_host.setText( host )
1471             server_port.setText( port )
1472             server_protocol.setCurrentIndex(protocol_letters.index(protocol))
1473
1474             if not plist: return
1475             for p in protocol_letters:
1476                 i = protocol_letters.index(p)
1477                 j = server_protocol.model().index(i,0)
1478                 if p not in pp.keys():
1479                     server_protocol.model().setData(j, QtCore.QVariant(0), QtCore.Qt.UserRole-1)
1480                 else:
1481                     server_protocol.model().setData(j, QtCore.QVariant(0,False), QtCore.Qt.UserRole-1)
1482
1483
1484         change_server(host,protocol)
1485         servers_list_widget.connect(servers_list_widget, SIGNAL('itemClicked(QTreeWidgetItem*, int)'), lambda x: change_server(unicode(x.text(0))))
1486         grid.addWidget(servers_list_widget, 1, 1, 1, 3)
1487
1488         if not wallet.config.is_modifiable('server'):
1489             for w in [server_host, server_port, server_protocol, servers_list_widget]: w.setEnabled(False)
1490
1491         # proxy setting
1492         proxy_mode = QComboBox()
1493         proxy_host = QLineEdit()
1494         proxy_host.setFixedWidth(200)
1495         proxy_port = QLineEdit()
1496         proxy_port.setFixedWidth(60)
1497         proxy_mode.addItems(['NONE', 'SOCKS4', 'SOCKS5', 'HTTP'])
1498
1499         def check_for_disable(index = False):
1500             if proxy_mode.currentText() != 'NONE':
1501                 proxy_host.setEnabled(True)
1502                 proxy_port.setEnabled(True)
1503             else:
1504                 proxy_host.setEnabled(False)
1505                 proxy_port.setEnabled(False)
1506
1507         check_for_disable()
1508         proxy_mode.connect(proxy_mode, SIGNAL('currentIndexChanged(int)'), check_for_disable)
1509
1510         if not wallet.config.is_modifiable('proxy'):
1511             for w in [proxy_host, proxy_port, proxy_mode]: w.setEnabled(False)
1512
1513         proxy_config = interface.proxy if interface.proxy else { "mode":"none", "host":"localhost", "port":"8080"}
1514         proxy_mode.setCurrentIndex(proxy_mode.findText(str(proxy_config.get("mode").upper())))
1515         proxy_host.setText(proxy_config.get("host"))
1516         proxy_port.setText(proxy_config.get("port"))
1517
1518         grid.addWidget(QLabel(_('Proxy') + ':'), 2, 0)
1519         grid.addWidget(proxy_mode, 2, 1)
1520         grid.addWidget(proxy_host, 2, 2)
1521         grid.addWidget(proxy_port, 2, 3)
1522
1523         # buttons
1524         vbox.addLayout(ok_cancel_buttons(d))
1525         d.setLayout(vbox) 
1526
1527         if not d.exec_(): return
1528
1529         server = unicode( server_host.text() ) + ':' + unicode( server_port.text() ) + ':' + (protocol_letters[server_protocol.currentIndex()])
1530         if proxy_mode.currentText() != 'NONE':
1531             proxy = { u'mode':unicode(proxy_mode.currentText()).lower(), u'host':unicode(proxy_host.text()), u'port':unicode(proxy_port.text()) }
1532         else:
1533             proxy = None
1534
1535         wallet.config.set_key("proxy", proxy, True)
1536         wallet.config.set_key("server", server, True)
1537         interface.set_server(server, proxy)
1538                 
1539         return True
1540
1541     def closeEvent(self, event):
1542         g = self.geometry()
1543         self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
1544         event.accept()
1545
1546
1547 class ElectrumGui:
1548
1549     def __init__(self, wallet, config, app=None):
1550         self.wallet = wallet
1551         self.config = config
1552         if app is None:
1553             self.app = QApplication(sys.argv)
1554
1555     def server_list_changed(self):
1556         pass
1557
1558
1559     def restore_or_create(self):
1560
1561         msg = _("Wallet file not found.")+"\n"+_("Do you want to create a new wallet, or to restore an existing one?")
1562         r = QMessageBox.question(None, _('Message'), msg, _('Create'), _('Restore'), _('Cancel'), 0, 2)
1563         if r==2: return False
1564         
1565         is_recovery = (r==1)
1566         wallet = self.wallet
1567         # ask for the server.
1568         if not ElectrumWindow.network_dialog( wallet, parent=None ): return False
1569
1570         # wait until we are connected, because the user might have selected another server
1571         if not wallet.interface.is_connected:
1572             waiting = lambda: False if wallet.interface.is_connected else "connecting...\n"
1573             waiting_dialog(waiting)
1574
1575         waiting = lambda: False if wallet.up_to_date else "Please wait...\nAddresses generated: %d\nKilobytes received: %.1f"\
1576             %(len(wallet.all_addresses()), wallet.interface.bytes_received/1024.)
1577
1578         if not is_recovery:
1579             wallet.new_seed(None)
1580             wallet.init_mpk( wallet.seed )
1581             wallet.up_to_date_event.clear()
1582             wallet.up_to_date = False
1583             wallet.interface.poke('synchronizer')
1584             waiting_dialog(waiting)
1585             # run a dialog indicating the seed, ask the user to remember it
1586             ElectrumWindow.show_seed_dialog(wallet)
1587             #ask for password
1588             ElectrumWindow.change_password_dialog(wallet)
1589         else:
1590             # ask for seed and gap.
1591             if not ElectrumWindow.seed_dialog( wallet ): return False
1592             wallet.init_mpk( wallet.seed )
1593             wallet.up_to_date_event.clear()
1594             wallet.up_to_date = False
1595             wallet.interface.poke('synchronizer')
1596             waiting_dialog(waiting)
1597             if wallet.is_found():
1598                 # history and addressbook
1599                 wallet.fill_addressbook()
1600                 print "Recovery successful"
1601                 wallet.save()
1602             else:
1603                 QMessageBox.information(None, _('Error'), _("No transactions found for this seed"), _('OK'))
1604
1605         wallet.save()
1606         return True
1607
1608     def main(self,url):
1609         s = Timer()
1610         s.start()
1611         w = ElectrumWindow(self.wallet, self.config)
1612         if url: w.set_url(url)
1613         w.app = self.app
1614         w.connect_slots(s)
1615         w.update_wallet()
1616         w.show()
1617
1618         self.app.exec_()