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