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