05964ae998da9c318674770c242678f229e7d2d3
[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             for tx_hash, tx_height in h:
849                 tx = self.wallet.transactions.get(tx_hash)
850                 if tx: n += 1
851
852             tx = "%d "%n
853             if n==0:
854                 if address in self.wallet.addresses:
855                     gap += 1
856                     if gap > self.wallet.gap_limit:
857                         is_red = True
858             else:
859                 if address in self.wallet.addresses:
860                     gap = 0
861
862             c, u = self.wallet.get_addr_balance(address)
863             balance = format_satoshis( c + u, False, self.wallet.num_zeros )
864             flags = self.wallet.get_address_flags(address)
865             item = QTreeWidgetItem( [ flags, address, label, balance, tx] )
866
867             item.setFont(0, QFont(MONOSPACE_FONT))
868             item.setFont(1, QFont(MONOSPACE_FONT))
869             item.setFont(3, QFont(MONOSPACE_FONT))
870             if address in self.wallet.frozen_addresses: 
871                 item.setBackgroundColor(1, QColor('lightblue'))
872             elif address in self.wallet.prioritized_addresses: 
873                 item.setBackgroundColor(1, QColor('lightgreen'))
874             if is_red and address in self.wallet.addresses:
875                 item.setBackgroundColor(1, QColor('red'))
876             l.addTopLevelItem(item)
877
878         # we use column 1 because column 0 may be hidden
879         l.setCurrentItem(l.topLevelItem(0),1)
880
881     def show_contact_details(self, m):
882         a = self.wallet.aliases.get(m)
883         if a:
884             if a[0] in self.wallet.authorities.keys():
885                 s = self.wallet.authorities.get(a[0])
886             else:
887                 s = "self-signed"
888             msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0]
889             QMessageBox.information(self, 'Alias', msg, 'OK')
890
891     def update_contacts_tab(self):
892
893         l = self.contacts_list
894         l.clear()
895         l.setColumnHidden(2, not self.detailed_view)
896         l.setColumnWidth(0, 350) 
897         l.setColumnWidth(1, 330)
898         l.setColumnWidth(2, 100) 
899
900         alias_targets = []
901         for alias, v in self.wallet.aliases.items():
902             s, target = v
903             alias_targets.append(target)
904             item = QTreeWidgetItem( [ target, alias, '-'] )
905             item.setBackgroundColor(0, QColor('lightgray'))
906             l.addTopLevelItem(item)
907             
908         for address in self.wallet.addressbook:
909             if address in alias_targets: continue
910             label = self.wallet.labels.get(address,'')
911             n = 0 
912             for item in self.wallet.transactions.values():
913                 if address in item['outputs'] : n=n+1
914             tx = "%d"%n
915             item = QTreeWidgetItem( [ address, label, tx] )
916             item.setFont(0, QFont(MONOSPACE_FONT))
917             l.addTopLevelItem(item)
918
919         l.setCurrentItem(l.topLevelItem(0))
920
921     def create_wall_tab(self):
922         self.textbox = textbox = QTextEdit(self)
923         textbox.setFont(QFont(MONOSPACE_FONT))
924         textbox.setReadOnly(True)
925         return textbox
926
927     def create_status_bar(self):
928         sb = QStatusBar()
929         sb.setFixedHeight(35)
930         if self.wallet.seed:
931             sb.addPermanentWidget( StatusBarButton( QIcon(":icons/lock.png"), "Password", lambda: self.change_password_dialog(self.wallet, self) ) )
932         sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), "Preferences", self.settings_dialog ) )
933         if self.wallet.seed:
934             sb.addPermanentWidget( StatusBarButton( QIcon(":icons/seed.png"), "Seed", lambda: self.show_seed_dialog(self.wallet, self) ) )
935         self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), "Network", lambda: self.network_dialog(self.wallet, self) ) 
936         sb.addPermanentWidget( self.status_button )
937         self.setStatusBar(sb)
938
939     def new_contact_dialog(self):
940         text, ok = QInputDialog.getText(self, _('New Contact'), _('Address') + ':')
941         address = unicode(text)
942         if ok:
943             if self.wallet.is_valid(address):
944                 self.wallet.addressbook.append(address)
945                 self.wallet.save()
946                 self.update_contacts_tab()
947                 self.update_history_tab()
948                 self.update_completions()
949             else:
950                 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
951
952     @staticmethod
953     def show_seed_dialog(wallet, parent=None):
954         if not wallet.seed:
955             QMessageBox.information(parent, _('Message'),
956                                     _('No seed'), _('OK'))
957             return
958
959         if wallet.use_encryption:
960             password = parent.password_dialog()
961             if not password:
962                 return
963         else:
964             password = None
965             
966         try:
967             seed = wallet.pw_decode(wallet.seed, password)
968         except:
969             QMessageBox.warning(parent, _('Error'),
970                                 _('Incorrect Password'), _('OK'))
971             return
972
973         dialog = QDialog(None)
974         dialog.setModal(1)
975         dialog.setWindowTitle("Electrum")
976
977         brainwallet = ' '.join(mnemonic.mn_encode(seed))
978
979         msg =   _("Your wallet generation seed is") +":<p>\"" + brainwallet + "\"<p>" \
980               + _("Please write down or memorize these 12 words (order is important).") + " " \
981               + _("This seed will allow you to recover your wallet in case of computer failure.") + "<p>" \
982               + _("WARNING: Never disclose your seed. Never type it on a website.") + "<p>"
983
984         main_text = QLabel(msg)
985         main_text.setWordWrap(True)
986
987         logo = QLabel()
988         logo.setPixmap(QPixmap(":icons/seed.png").scaledToWidth(56))
989
990         if parent:
991             app = parent.app
992         else:
993             app = QApplication
994
995         copy_function = lambda: app.clipboard().setText(brainwallet)
996         copy_button = QPushButton(_("Copy to Clipboard"))
997         copy_button.clicked.connect(copy_function)
998
999         show_qr_function = lambda: ElectrumWindow.show_seed_qrcode(seed)
1000         qr_button = QPushButton(_("View as QR Code"))
1001         qr_button.clicked.connect(show_qr_function)
1002
1003         ok_button = QPushButton(_("OK"))
1004         ok_button.setDefault(True)
1005         ok_button.clicked.connect(dialog.accept)
1006
1007         main_layout = QGridLayout()
1008         main_layout.addWidget(logo, 0, 0)
1009         main_layout.addWidget(main_text, 0, 1, 1, -1)
1010         main_layout.addWidget(copy_button, 1, 1)
1011         main_layout.addWidget(qr_button, 1, 2)
1012         main_layout.addWidget(ok_button, 1, 3)
1013         dialog.setLayout(main_layout)
1014
1015         dialog.exec_()
1016
1017     @staticmethod
1018     def show_seed_qrcode(seed):
1019         if not seed: return
1020         d = QDialog(None)
1021         d.setModal(1)
1022         d.setWindowTitle(_("Seed"))
1023         d.setMinimumSize(270, 300)
1024         vbox = QVBoxLayout()
1025         vbox.addWidget(QRCodeWidget(seed))
1026         hbox = QHBoxLayout()
1027         hbox.addStretch(1)
1028         b = QPushButton(_("OK"))
1029         hbox.addWidget(b)
1030         b.clicked.connect(d.accept)
1031
1032         vbox.addLayout(hbox)
1033         d.setLayout(vbox)
1034         d.exec_()
1035
1036
1037     def show_address_qrcode(self,address):
1038         if not address: return
1039         d = QDialog(self)
1040         d.setModal(1)
1041         d.setWindowTitle(address)
1042         d.setMinimumSize(270, 350)
1043         vbox = QVBoxLayout()
1044         qrw = QRCodeWidget(address)
1045         vbox.addWidget(qrw)
1046
1047         hbox = QHBoxLayout()
1048         amount_e = QLineEdit()
1049         hbox.addWidget(QLabel(_('Amount')))
1050         hbox.addWidget(amount_e)
1051         vbox.addLayout(hbox)
1052
1053         #hbox = QHBoxLayout()
1054         #label_e = QLineEdit()
1055         #hbox.addWidget(QLabel('Label'))
1056         #hbox.addWidget(label_e)
1057         #vbox.addLayout(hbox)
1058
1059         def amount_changed():
1060             amount = numbify(amount_e)
1061             #label = str( label_e.getText() )
1062             if amount is not None:
1063                 qrw.set_addr('bitcoin:%s?amount=%s'%(address,str( Decimal(amount) /100000000)))
1064             else:
1065                 qrw.set_addr( address )
1066             qrw.repaint()
1067
1068         def do_save():
1069             bmp.save_qrcode(qrw.qr, "qrcode.bmp")
1070             self.show_message(_("QR code saved to file") + " 'qrcode.bmp'")
1071             
1072         amount_e.textChanged.connect( amount_changed )
1073
1074         hbox = QHBoxLayout()
1075         hbox.addStretch(1)
1076         b = QPushButton(_("Save"))
1077         b.clicked.connect(do_save)
1078         hbox.addWidget(b)
1079         b = QPushButton(_("Close"))
1080         hbox.addWidget(b)
1081         b.clicked.connect(d.accept)
1082
1083         vbox.addLayout(hbox)
1084         d.setLayout(vbox)
1085         d.exec_()
1086
1087     def question(self, msg):
1088         return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1089
1090     def show_message(self, msg):
1091         QMessageBox.information(self, _('Message'), msg, _('OK'))
1092
1093     def password_dialog(self ):
1094         d = QDialog(self)
1095         d.setModal(1)
1096
1097         pw = QLineEdit()
1098         pw.setEchoMode(2)
1099
1100         vbox = QVBoxLayout()
1101         msg = _('Please enter your password')
1102         vbox.addWidget(QLabel(msg))
1103
1104         grid = QGridLayout()
1105         grid.setSpacing(8)
1106         grid.addWidget(QLabel(_('Password')), 1, 0)
1107         grid.addWidget(pw, 1, 1)
1108         vbox.addLayout(grid)
1109
1110         vbox.addLayout(ok_cancel_buttons(d))
1111         d.setLayout(vbox) 
1112
1113         if not d.exec_(): return
1114         return unicode(pw.text())
1115
1116
1117
1118
1119
1120     @staticmethod
1121     def change_password_dialog( wallet, parent=None ):
1122
1123         if not wallet.seed:
1124             QMessageBox.information(parent, _('Error'), _('No seed'), _('OK'))
1125             return
1126
1127         d = QDialog(parent)
1128         d.setModal(1)
1129
1130         pw = QLineEdit()
1131         pw.setEchoMode(2)
1132         new_pw = QLineEdit()
1133         new_pw.setEchoMode(2)
1134         conf_pw = QLineEdit()
1135         conf_pw.setEchoMode(2)
1136
1137         vbox = QVBoxLayout()
1138         if parent:
1139             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')
1140         else:
1141             msg = _("Please choose a password to encrypt your wallet keys.")+'\n'+_("Leave these fields empty if you want to disable encryption.")
1142         vbox.addWidget(QLabel(msg))
1143
1144         grid = QGridLayout()
1145         grid.setSpacing(8)
1146
1147         if wallet.use_encryption:
1148             grid.addWidget(QLabel(_('Password')), 1, 0)
1149             grid.addWidget(pw, 1, 1)
1150
1151         grid.addWidget(QLabel(_('New Password')), 2, 0)
1152         grid.addWidget(new_pw, 2, 1)
1153
1154         grid.addWidget(QLabel(_('Confirm Password')), 3, 0)
1155         grid.addWidget(conf_pw, 3, 1)
1156         vbox.addLayout(grid)
1157
1158         vbox.addLayout(ok_cancel_buttons(d))
1159         d.setLayout(vbox) 
1160
1161         if not d.exec_(): return
1162
1163         password = unicode(pw.text()) if wallet.use_encryption else None
1164         new_password = unicode(new_pw.text())
1165         new_password2 = unicode(conf_pw.text())
1166
1167         try:
1168             seed = wallet.pw_decode( wallet.seed, password)
1169         except:
1170             QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
1171             return
1172
1173         if new_password != new_password2:
1174             QMessageBox.warning(parent, _('Error'), _('Passwords do not match'), _('OK'))
1175             return
1176
1177         wallet.update_password(seed, password, new_password)
1178
1179     @staticmethod
1180     def seed_dialog(wallet, parent=None):
1181         d = QDialog(parent)
1182         d.setModal(1)
1183
1184         vbox = QVBoxLayout()
1185         msg = _("Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet.")
1186         vbox.addWidget(QLabel(msg))
1187
1188         grid = QGridLayout()
1189         grid.setSpacing(8)
1190
1191         seed_e = QLineEdit()
1192         grid.addWidget(QLabel(_('Seed or mnemonic')), 1, 0)
1193         grid.addWidget(seed_e, 1, 1)
1194
1195         gap_e = QLineEdit()
1196         gap_e.setText("5")
1197         grid.addWidget(QLabel(_('Gap limit')), 2, 0)
1198         grid.addWidget(gap_e, 2, 1)
1199         gap_e.textChanged.connect(lambda: numbify(gap_e,True))
1200         vbox.addLayout(grid)
1201
1202         vbox.addLayout(ok_cancel_buttons(d))
1203         d.setLayout(vbox) 
1204
1205         if not d.exec_(): return
1206
1207         try:
1208             gap = int(unicode(gap_e.text()))
1209         except:
1210             QMessageBox.warning(None, _('Error'), 'error', 'OK')
1211             sys.exit(0)
1212
1213         try:
1214             seed = unicode(seed_e.text())
1215             seed.decode('hex')
1216         except:
1217             print_error("Warning: Not hex, trying decode")
1218             try:
1219                 seed = mnemonic.mn_decode( seed.split(' ') )
1220             except:
1221                 QMessageBox.warning(None, _('Error'), _('I cannot decode this'), _('OK'))
1222                 sys.exit(0)
1223         if not seed:
1224             QMessageBox.warning(None, _('Error'), _('No seed'), 'OK')
1225             sys.exit(0)
1226         
1227         wallet.seed = str(seed)
1228         #print repr(wallet.seed)
1229         wallet.gap_limit = gap
1230         return True
1231
1232
1233
1234     def settings_dialog(self):
1235         d = QDialog(self)
1236         d.setModal(1)
1237         vbox = QVBoxLayout()
1238         msg = _('Here are the settings of your wallet.') + '\n'\
1239               + _('For more explanations, click on the help buttons next to each field.')
1240
1241         label = QLabel(msg)
1242         label.setFixedWidth(250)
1243         label.setWordWrap(True)
1244         label.setAlignment(Qt.AlignJustify)
1245         vbox.addWidget(label)
1246
1247         grid = QGridLayout()
1248         grid.setSpacing(8)
1249         vbox.addLayout(grid)
1250
1251         fee_label = QLabel(_('Transaction fee'))
1252         grid.addWidget(fee_label, 2, 0)
1253         fee_e = QLineEdit()
1254         fee_e.setText("%s"% str( Decimal( self.wallet.fee)/100000000 ) )
1255         grid.addWidget(fee_e, 2, 1)
1256         msg = _('Fee per transaction input. Transactions involving multiple inputs tend to require a higher fee.') + ' ' \
1257             + _('Recommended value') + ': 0.001'
1258         grid.addWidget(HelpButton(msg), 2, 2)
1259         fee_e.textChanged.connect(lambda: numbify(fee_e,False))
1260         if not self.config.is_modifiable('fee'):
1261             for w in [fee_e, fee_label]: w.setEnabled(False)
1262
1263         nz_label = QLabel(_('Display zeros'))
1264         grid.addWidget(nz_label, 3, 0)
1265         nz_e = QLineEdit()
1266         nz_e.setText("%d"% self.wallet.num_zeros)
1267         grid.addWidget(nz_e, 3, 1)
1268         msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1269         grid.addWidget(HelpButton(msg), 3, 2)
1270         nz_e.textChanged.connect(lambda: numbify(nz_e,True))
1271         if not self.config.is_modifiable('num_zeros'):
1272             for w in [nz_e, nz_label]: w.setEnabled(False)
1273
1274         usechange_cb = QCheckBox(_('Use change addresses'))
1275         grid.addWidget(usechange_cb, 5, 0)
1276         usechange_cb.setChecked(self.wallet.use_change)
1277         grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions. ')), 5, 2)
1278         if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
1279
1280         gap_label = QLabel(_('Gap limit'))
1281         grid.addWidget(gap_label, 6, 0)
1282         gap_e = QLineEdit()
1283         gap_e.setText("%d"% self.wallet.gap_limit)
1284         grid.addWidget(gap_e, 6, 1)
1285         msg =  _('The gap limit is the maximal number of contiguous unused addresses in your sequence of receiving addresses.') + '\n' \
1286               + _('You may increase it if you need more receiving addresses.') + '\n\n' \
1287               + _('Your current gap limit is') + ': %d'%self.wallet.gap_limit + '\n' \
1288               + _('Given the current status of your address sequence, the minimum gap limit you can use is: ') + '%d'%self.wallet.min_acceptable_gap() + '\n\n' \
1289               + _('Warning') + ': ' \
1290               + _('The gap limit parameter must be provided in order to recover your wallet from seed.') + ' ' \
1291               + _('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' 
1292         grid.addWidget(HelpButton(msg), 6, 2)
1293         gap_e.textChanged.connect(lambda: numbify(nz_e,True))
1294         if not self.config.is_modifiable('gap_limit'):
1295             for w in [gap_e, gap_label]: w.setEnabled(False)
1296         
1297         gui_label=QLabel(_('Default GUI') + ':')
1298         grid.addWidget(gui_label , 7, 0)
1299         gui_combo = QComboBox()
1300         gui_combo.addItems(['Lite', 'Classic', 'Gtk', 'Text'])
1301         index = gui_combo.findText(self.config.get("gui","classic").capitalize())
1302         if index==-1: index = 1
1303         gui_combo.setCurrentIndex(index)
1304         grid.addWidget(gui_combo, 7, 1)
1305         grid.addWidget(HelpButton(_('Select which GUI mode to use at start up. ')), 7, 2)
1306         if not self.config.is_modifiable('gui'):
1307             for w in [gui_combo, gui_label]: w.setEnabled(False)
1308
1309         vbox.addLayout(ok_cancel_buttons(d))
1310         d.setLayout(vbox) 
1311
1312         # run the dialog
1313         if not d.exec_(): return
1314
1315         fee = unicode(fee_e.text())
1316         try:
1317             fee = int( 100000000 * Decimal(fee) )
1318         except:
1319             QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
1320             return
1321
1322         if self.wallet.fee != fee:
1323             self.wallet.fee = fee
1324             self.wallet.save()
1325         
1326         nz = unicode(nz_e.text())
1327         try:
1328             nz = int( nz )
1329             if nz>8: nz=8
1330         except:
1331             QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
1332             return
1333
1334         if self.wallet.num_zeros != nz:
1335             self.wallet.num_zeros = nz
1336             self.config.set_key('num_zeros', nz, True)
1337             self.update_history_tab()
1338             self.update_receive_tab()
1339
1340         if self.wallet.use_change != usechange_cb.isChecked():
1341             self.wallet.use_change = usechange_cb.isChecked()
1342             self.config.set_key('use_change', self.wallet.use_change, True)
1343         
1344         try:
1345             n = int(gap_e.text())
1346         except:
1347             QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
1348             return
1349
1350         if self.wallet.gap_limit != n:
1351             r = self.wallet.change_gap_limit(n)
1352             if r:
1353                 self.update_receive_tab()
1354                 self.config.set_key('gap_limit', self.wallet.gap_limit, True)
1355             else:
1356                 QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
1357                     
1358         self.config.set_key("gui", str(gui_combo.currentText()).lower(), True)
1359
1360
1361
1362     @staticmethod 
1363     def network_dialog(wallet, parent=None):
1364         interface = wallet.interface
1365         if parent:
1366             if interface.is_connected:
1367                 status = _("Connected to")+" %s\n%d blocks"%(interface.host, wallet.verifier.height)
1368             else:
1369                 status = _("Not connected")
1370         else:
1371             import random
1372             status = _("Please choose a server.")
1373
1374         server = interface.server
1375
1376         plist = {}
1377         if not wallet.interface.servers:
1378             servers_list = []
1379             for x in DEFAULT_SERVERS:
1380                 h,port,protocol = x.split(':')
1381                 servers_list.append( (h,[(protocol,port)] ) )
1382         else:
1383             servers_list = wallet.interface.servers
1384             for item in servers_list:
1385                 _host, pp = item
1386                 z = {}
1387                 for item2 in pp:
1388                     _protocol, _port = item2
1389                     z[_protocol] = _port
1390                 plist[_host] = z
1391
1392         d = QDialog(parent)
1393         d.setModal(1)
1394         d.setWindowTitle(_('Server'))
1395         d.setMinimumSize(375, 20)
1396
1397         vbox = QVBoxLayout()
1398         vbox.setSpacing(30)
1399
1400         hbox = QHBoxLayout()
1401         l = QLabel()
1402         l.setPixmap(QPixmap(":icons/network.png"))
1403         hbox.addStretch(10)
1404         hbox.addWidget(l)
1405         hbox.addWidget(QLabel(status))
1406         hbox.addStretch(50)
1407         vbox.addLayout(hbox)
1408
1409
1410         # grid layout
1411         grid = QGridLayout()
1412         grid.setSpacing(8)
1413         vbox.addLayout(grid)
1414
1415         # server
1416         server_protocol = QComboBox()
1417         server_host = QLineEdit()
1418         server_host.setFixedWidth(200)
1419         server_port = QLineEdit()
1420         server_port.setFixedWidth(60)
1421
1422         protocol_names = ['TCP', 'HTTP', 'TCP/SSL', 'HTTPS']
1423         protocol_letters = 'thsg'
1424         DEFAULT_PORTS = {'t':'50001', 's':'50002', 'h':'8081', 'g':'8082'}
1425         server_protocol.addItems(protocol_names)
1426
1427         grid.addWidget(QLabel(_('Server') + ':'), 0, 0)
1428         grid.addWidget(server_protocol, 0, 1)
1429         grid.addWidget(server_host, 0, 2)
1430         grid.addWidget(server_port, 0, 3)
1431
1432         host, port, protocol = server.split(':')
1433
1434         def change_protocol(p):
1435             protocol = protocol_letters[p]
1436             host = unicode(server_host.text())
1437             pp = plist.get(host,DEFAULT_PORTS)
1438             if protocol not in pp.keys():
1439                 protocol = pp.keys()[0]
1440             port = pp[protocol]
1441             server_host.setText( host )
1442             server_port.setText( port )
1443
1444         server_protocol.connect(server_protocol, SIGNAL('currentIndexChanged(int)'), change_protocol)
1445         
1446         label = _('Active Servers') if wallet.interface.servers else _('Default Servers')
1447         servers_list_widget = QTreeWidget(parent)
1448         servers_list_widget.setHeaderLabels( [ label ] )
1449         servers_list_widget.setMaximumHeight(150)
1450         for _host, _x in servers_list:
1451             servers_list_widget.addTopLevelItem(QTreeWidgetItem( [ _host ] ))
1452
1453
1454         def change_server(host, protocol=None):
1455             pp = plist.get(host,DEFAULT_PORTS)
1456             if protocol:
1457                 port = pp.get(protocol)
1458                 if not port: protocol = None
1459                     
1460             if not protocol:
1461                 if 't' in pp.keys():
1462                     protocol = 't'
1463                     port = pp.get(protocol)
1464                 else:
1465                     protocol = pp.keys()[0]
1466                     port = pp.get(protocol)
1467             
1468             server_host.setText( host )
1469             server_port.setText( port )
1470             server_protocol.setCurrentIndex(protocol_letters.index(protocol))
1471
1472             if not plist: return
1473             for p in protocol_letters:
1474                 i = protocol_letters.index(p)
1475                 j = server_protocol.model().index(i,0)
1476                 if p not in pp.keys():
1477                     server_protocol.model().setData(j, QtCore.QVariant(0), QtCore.Qt.UserRole-1)
1478                 else:
1479                     server_protocol.model().setData(j, QtCore.QVariant(0,False), QtCore.Qt.UserRole-1)
1480
1481
1482         change_server(host,protocol)
1483         servers_list_widget.connect(servers_list_widget, SIGNAL('itemClicked(QTreeWidgetItem*, int)'), lambda x: change_server(unicode(x.text(0))))
1484         grid.addWidget(servers_list_widget, 1, 1, 1, 3)
1485
1486         if not wallet.config.is_modifiable('server'):
1487             for w in [server_host, server_port, server_protocol, servers_list_widget]: w.setEnabled(False)
1488
1489         # proxy setting
1490         proxy_mode = QComboBox()
1491         proxy_host = QLineEdit()
1492         proxy_host.setFixedWidth(200)
1493         proxy_port = QLineEdit()
1494         proxy_port.setFixedWidth(60)
1495         proxy_mode.addItems(['NONE', 'SOCKS4', 'SOCKS5', 'HTTP'])
1496
1497         def check_for_disable(index = False):
1498             if proxy_mode.currentText() != 'NONE':
1499                 proxy_host.setEnabled(True)
1500                 proxy_port.setEnabled(True)
1501             else:
1502                 proxy_host.setEnabled(False)
1503                 proxy_port.setEnabled(False)
1504
1505         check_for_disable()
1506         proxy_mode.connect(proxy_mode, SIGNAL('currentIndexChanged(int)'), check_for_disable)
1507
1508         if not wallet.config.is_modifiable('proxy'):
1509             for w in [proxy_host, proxy_port, proxy_mode]: w.setEnabled(False)
1510
1511         proxy_config = interface.proxy if interface.proxy else { "mode":"none", "host":"localhost", "port":"8080"}
1512         proxy_mode.setCurrentIndex(proxy_mode.findText(str(proxy_config.get("mode").upper())))
1513         proxy_host.setText(proxy_config.get("host"))
1514         proxy_port.setText(proxy_config.get("port"))
1515
1516         grid.addWidget(QLabel(_('Proxy') + ':'), 2, 0)
1517         grid.addWidget(proxy_mode, 2, 1)
1518         grid.addWidget(proxy_host, 2, 2)
1519         grid.addWidget(proxy_port, 2, 3)
1520
1521         # buttons
1522         vbox.addLayout(ok_cancel_buttons(d))
1523         d.setLayout(vbox) 
1524
1525         if not d.exec_(): return
1526
1527         server = unicode( server_host.text() ) + ':' + unicode( server_port.text() ) + ':' + (protocol_letters[server_protocol.currentIndex()])
1528         if proxy_mode.currentText() != 'NONE':
1529             proxy = { u'mode':unicode(proxy_mode.currentText()).lower(), u'host':unicode(proxy_host.text()), u'port':unicode(proxy_port.text()) }
1530         else:
1531             proxy = None
1532
1533         wallet.config.set_key("proxy", proxy, True)
1534         wallet.config.set_key("server", server, True)
1535         interface.set_server(server, proxy)
1536                 
1537         return True
1538
1539     def closeEvent(self, event):
1540         g = self.geometry()
1541         self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
1542         event.accept()
1543
1544
1545 class ElectrumGui:
1546
1547     def __init__(self, wallet, config, app=None):
1548         self.wallet = wallet
1549         self.config = config
1550         if app is None:
1551             self.app = QApplication(sys.argv)
1552
1553     def server_list_changed(self):
1554         pass
1555
1556
1557     def restore_or_create(self):
1558
1559         msg = _("Wallet file not found.")+"\n"+_("Do you want to create a new wallet, or to restore an existing one?")
1560         r = QMessageBox.question(None, _('Message'), msg, _('Create'), _('Restore'), _('Cancel'), 0, 2)
1561         if r==2: return False
1562         
1563         is_recovery = (r==1)
1564         wallet = self.wallet
1565         # ask for the server.
1566         if not ElectrumWindow.network_dialog( wallet, parent=None ): return False
1567
1568         # wait until we are connected, because the user might have selected another server
1569         if not wallet.interface.is_connected:
1570             waiting = lambda: False if wallet.interface.is_connected else "connecting...\n"
1571             waiting_dialog(waiting)
1572
1573         waiting = lambda: False if wallet.up_to_date else "Please wait...\nAddresses generated: %d\nKilobytes received: %.1f"\
1574             %(len(wallet.all_addresses()), wallet.interface.bytes_received/1024.)
1575
1576         if not is_recovery:
1577             wallet.new_seed(None)
1578             wallet.init_mpk( wallet.seed )
1579             wallet.up_to_date_event.clear()
1580             wallet.up_to_date = False
1581             wallet.interface.poke('synchronizer')
1582             waiting_dialog(waiting)
1583             # run a dialog indicating the seed, ask the user to remember it
1584             ElectrumWindow.show_seed_dialog(wallet)
1585             #ask for password
1586             ElectrumWindow.change_password_dialog(wallet)
1587         else:
1588             # ask for seed and gap.
1589             if not ElectrumWindow.seed_dialog( wallet ): return False
1590             wallet.init_mpk( wallet.seed )
1591             wallet.up_to_date_event.clear()
1592             wallet.up_to_date = False
1593             wallet.interface.poke('synchronizer')
1594             waiting_dialog(waiting)
1595             if wallet.is_found():
1596                 # history and addressbook
1597                 wallet.fill_addressbook()
1598                 print "Recovery successful"
1599                 wallet.save()
1600             else:
1601                 QMessageBox.information(None, _('Error'), _("No transactions found for this seed"), _('OK'))
1602
1603         wallet.save()
1604         return True
1605
1606     def main(self,url):
1607         s = Timer()
1608         s.start()
1609         w = ElectrumWindow(self.wallet, self.config)
1610         if url: w.set_url(url)
1611         w.app = self.app
1612         w.connect_slots(s)
1613         w.update_wallet()
1614         w.show()
1615
1616         self.app.exec_()