do not display requested amount if zero. show menu item only if window is visible
[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.setMinimumSize(210, 210)
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(self.addr)
149         self.qr.make()
150         
151     def paintEvent(self, e):
152         if not self.addr: return
153         
154         qp = QtGui.QPainter()
155         qp.begin(self)
156         boxsize = 6
157         size = self.qr.getModuleCount()*boxsize
158         k = self.qr.getModuleCount()
159         black = QColor(0, 0, 0, 255)
160         white = QColor(255, 255, 255, 255)
161         for r in range(k):
162             for c in range(k):
163                 if self.qr.isDark(r, c):
164                     qp.setBrush(black)
165                     qp.setPen(black)
166                 else:
167                     qp.setBrush(white)
168                     qp.setPen(white)
169                 qp.drawRect(c*boxsize, r*boxsize, boxsize, boxsize)
170         qp.end()
171         
172
173
174 class QR_Window(QWidget):
175
176     def __init__(self):
177         QWidget.__init__(self)
178         self.setWindowTitle('Electrum - Invoice')
179         self.setMinimumSize(800, 250)
180         self.address = ''
181         self.labe = ''
182         self.amount = 0
183         self.setFocusPolicy(QtCore.Qt.NoFocus)
184
185         main_box = QHBoxLayout()
186         
187         self.qrw = QRCodeWidget('')
188         main_box.addWidget(self.qrw)
189
190         vbox = QVBoxLayout()
191         main_box.addLayout(vbox)
192
193         main_box.addStretch(1)
194
195         self.address_label = QLabel("")
196         self.address_label.setFont(QFont(MONOSPACE_FONT))
197         vbox.addWidget(self.address_label)
198
199         self.label_label = QLabel("")
200         vbox.addWidget(self.label_label)
201
202         self.amount_label = QLabel("")
203         vbox.addWidget(self.amount_label)
204
205         vbox.addStretch(1)
206
207         self.setLayout(main_box)
208
209     def do_save(self):
210         self.filename = "qrcode.bmp"
211         bmp.save_qrcode(self.qrw.qr, self.filename)
212
213     def set_content(self, addr, label, amount):
214         self.address = addr
215         address_text = "<span style='font-size: 21pt'>%s</span>" % addr if addr else ""
216         self.address_label.setText(address_text)
217
218         self.amount = amount
219         amount_text = "<span style='font-size: 21pt'>%s</span> <span style='font-size: 16pt'>BTC</span> " % format_satoshis(amount) if amount else ""
220         self.amount_label.setText(amount_text)
221
222         self.label = label
223         label_text = "<span style='font-size: 21pt'>%s</span>" % label if label else ""
224         self.label_label.setText(label_text)
225
226         msg = 'bitcoin:'+self.address
227         if self.amount is not None:
228             msg += '?amount=%s'%(str( Decimal(self.amount) /100000000))
229             if self.label is not None:
230                 msg += '&label=%s'%(self.label)
231         elif self.label is not None:
232             msg += '?label=%s'%(self.label)
233             
234         self.qrw.set_addr( msg )
235         self.qrw.update()
236
237             
238
239
240 def waiting_dialog(f):
241
242     s = Timer()
243     s.start()
244     w = QDialog()
245     w.resize(200, 70)
246     w.setWindowTitle('Electrum')
247     l = QLabel('')
248     vbox = QVBoxLayout()
249     vbox.addWidget(l)
250     w.setLayout(vbox)
251     w.show()
252     def ff():
253         s = f()
254         if s: l.setText(s)
255         else: w.close()
256     w.connect(s, QtCore.SIGNAL('timersignal'), ff)
257     w.exec_()
258     w.destroy()
259
260
261 def ok_cancel_buttons(dialog):
262     hbox = QHBoxLayout()
263     hbox.addStretch(1)
264     b = QPushButton("OK")
265     hbox.addWidget(b)
266     b.clicked.connect(dialog.accept)
267     b = QPushButton("Cancel")
268     hbox.addWidget(b)
269     b.clicked.connect(dialog.reject)
270     return hbox
271
272
273 class ElectrumWindow(QMainWindow):
274
275     def __init__(self, wallet, config):
276         QMainWindow.__init__(self)
277         self.wallet = wallet
278         self.config = config
279         self.wallet.interface.register_callback('updated', self.update_callback)
280         self.wallet.interface.register_callback('connected', self.update_callback)
281         self.wallet.interface.register_callback('disconnected', self.update_callback)
282         self.wallet.interface.register_callback('disconnecting', self.update_callback)
283
284         self.detailed_view = config.get('qt_detailed_view', False)
285
286         self.qr_window = None
287         self.funds_error = False
288         self.completions = QStringListModel()
289
290         self.tabs = tabs = QTabWidget(self)
291         tabs.addTab(self.create_history_tab(), _('History') )
292         if self.wallet.seed:
293             tabs.addTab(self.create_send_tab(), _('Send') )
294         tabs.addTab(self.create_receive_tab(), _('Receive') )
295         tabs.addTab(self.create_contacts_tab(), _('Contacts') )
296         tabs.addTab(self.create_wall_tab(), _('Wall') )
297         tabs.setMinimumSize(600, 400)
298         tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
299         self.setCentralWidget(tabs)
300         self.create_status_bar()
301
302         g = self.config.get("winpos-qt",[100, 100, 840, 400])
303         self.setGeometry(g[0], g[1], g[2], g[3])
304         title = 'Electrum ' + self.wallet.electrum_version + '  -  ' + self.config.path
305         if not self.wallet.seed: title += ' [seedless]'
306         self.setWindowTitle( title )
307
308         QShortcut(QKeySequence("Ctrl+W"), self, self.close)
309         QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
310         QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
311         QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
312         
313         self.connect(self, QtCore.SIGNAL('updatesignal'), self.update_wallet)
314         #self.connect(self, SIGNAL('editamount'), self.edit_amount)
315         self.history_list.setFocus(True)
316
317         # dark magic fix by flatfly; https://bitcointalk.org/index.php?topic=73651.msg959913#msg959913
318         if platform.system() == 'Windows':
319             n = 3 if self.wallet.seed else 2
320             tabs.setCurrentIndex (n)
321             tabs.setCurrentIndex (0)
322
323     def close(self):
324         QMainWindow.close(self)
325         if self.qr_window: 
326             self.qr_window.close()
327             self.qr_window = None
328
329     def connect_slots(self, sender):
330         if self.wallet.seed:
331             self.connect(sender, QtCore.SIGNAL('timersignal'), self.check_recipient)
332             self.previous_payto_e=''
333
334     def check_recipient(self):
335         if self.payto_e.hasFocus():
336             return
337         r = unicode( self.payto_e.text() )
338         if r != self.previous_payto_e:
339             self.previous_payto_e = r
340             r = r.strip()
341             if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r):
342                 try:
343                     to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
344                 except:
345                     return
346                 if to_address:
347                     s = r + '  <' + to_address + '>'
348                     self.payto_e.setText(s)
349
350
351     def update_callback(self):
352         self.emit(QtCore.SIGNAL('updatesignal'))
353
354     def update_wallet(self):
355         if self.wallet.interface and self.wallet.interface.is_connected:
356             if not self.wallet.up_to_date:
357                 text = _( "Synchronizing..." )
358                 icon = QIcon(":icons/status_waiting.png")
359             else:
360                 c, u = self.wallet.get_balance()
361                 text =  _( "Balance" ) + ": %s "%( format_satoshis(c,False,self.wallet.num_zeros) )
362                 if u: text +=  "[%s unconfirmed]"%( format_satoshis(u,True,self.wallet.num_zeros).strip() )
363                 icon = QIcon(":icons/status_connected.png")
364         else:
365             text = _( "Not connected" )
366             icon = QIcon(":icons/status_disconnected.png")
367
368         if self.funds_error:
369             text = _( "Not enough funds" )
370
371         self.statusBar().showMessage(text)
372         self.status_button.setIcon( icon )
373
374         if self.wallet.up_to_date or not self.wallet.interface.is_connected:
375             self.textbox.setText( self.wallet.banner )
376             self.update_history_tab()
377             self.update_receive_tab()
378             self.update_contacts_tab()
379             self.update_completions()
380
381
382     def create_history_tab(self):
383         self.history_list = l = MyTreeWidget(self)
384         l.setColumnCount(5)
385         l.setColumnWidth(0, 40) 
386         l.setColumnWidth(1, 140) 
387         l.setColumnWidth(2, 350) 
388         l.setColumnWidth(3, 140) 
389         l.setColumnWidth(4, 140) 
390         l.setHeaderLabels( [ '', _( 'Date' ), _( 'Description' ) , _('Amount'), _('Balance')] )
391         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
392         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
393
394         l.setContextMenuPolicy(Qt.CustomContextMenu)
395         l.customContextMenuRequested.connect(self.create_history_menu)
396         return l
397
398
399     def create_history_menu(self, position):
400         self.history_list.selectedIndexes() 
401         item = self.history_list.currentItem()
402         if not item: return
403         tx_hash = str(item.toolTip(0))
404         if not tx_hash: return
405         menu = QMenu()
406         menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
407         menu.addAction(_("Details"), lambda: self.tx_details(tx_hash))
408         menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
409         menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
410
411
412     def tx_details(self, tx_hash):
413         tx_details = self.wallet.get_tx_details(tx_hash)
414         QMessageBox.information(self, 'Details', tx_details, 'OK')
415
416
417     def tx_label_clicked(self, item, column):
418         if column==2 and item.isSelected():
419             tx_hash = str(item.toolTip(0))
420             self.is_edit=True
421             #if not self.wallet.labels.get(tx_hash): item.setText(2,'')
422             item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
423             self.history_list.editItem( item, column )
424             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
425             self.is_edit=False
426
427     def tx_label_changed(self, item, column):
428         if self.is_edit: 
429             return
430         self.is_edit=True
431         tx_hash = str(item.toolTip(0))
432         tx = self.wallet.transactions.get(tx_hash)
433         s = self.wallet.labels.get(tx_hash)
434         text = unicode( item.text(2) )
435         if text: 
436             self.wallet.labels[tx_hash] = text
437             item.setForeground(2, QBrush(QColor('black')))
438         else:
439             if s: self.wallet.labels.pop(tx_hash)
440             text = self.wallet.get_default_label(tx_hash)
441             item.setText(2, text)
442             item.setForeground(2, QBrush(QColor('gray')))
443         self.is_edit=False
444
445
446     def edit_label(self, is_recv):
447         l = self.receive_list if is_recv else self.contacts_list
448         c = 2 if is_recv else 1
449         item = l.currentItem()
450         item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
451         l.editItem( item, c )
452         item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
453
454
455
456     def address_label_clicked(self, item, column, l, column_addr, column_label):
457         if column == column_label and item.isSelected():
458             addr = unicode( item.text(column_addr) )
459             label = unicode( item.text(column_label) )
460             if label in self.wallet.aliases.keys():
461                 return
462             item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
463             l.editItem( item, column )
464             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
465
466
467     def address_label_changed(self, item, column, l, column_addr, column_label):
468
469         if column == column_label:
470             addr = unicode( item.text(column_addr) )
471             text = unicode( item.text(column_label) )
472             changed = False
473
474             if text:
475                 if text not in self.wallet.aliases.keys():
476                     old_addr = self.wallet.labels.get(text)
477                     if old_addr != addr:
478                         self.wallet.labels[addr] = text
479                         changed = True
480                 else:
481                     print_error("Error: This is one of your aliases")
482                     label = self.wallet.labels.get(addr,'')
483                     item.setText(column_label, QString(label))
484             else:
485                 s = self.wallet.labels.get(addr)
486                 if s: 
487                     self.wallet.labels.pop(addr)
488                     changed = True
489
490             if changed:
491                 self.update_history_tab()
492                 self.update_completions()
493                 
494             self.recv_changed(item)
495
496
497     def recv_changed(self, a):
498         "current item changed"
499         if a is not None and self.qr_window and self.qr_window.isVisible():
500             address = str(a.text(1))
501             label = self.wallet.labels.get(address)
502             amount = self.wallet.requested_amounts.get(address)
503             self.qr_window.set_content( address, label, amount )
504
505
506     def update_history_tab(self):
507
508         self.history_list.clear()
509         for item in self.wallet.get_tx_history():
510             tx_hash, conf, is_mine, value, fee, balance, timestamp = item
511             if conf:
512                 try:
513                     time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
514                 except:
515                     time_str = "unknown"
516                 if conf == -1:
517                     icon = None
518                 if conf == 0:
519                     icon = QIcon(":icons/unconfirmed.png")
520                 elif conf < 6:
521                     icon = QIcon(":icons/clock%d.png"%conf)
522                 else:
523                     icon = QIcon(":icons/confirmed.png")
524             else:
525                 time_str = 'pending'
526                 icon = QIcon(":icons/unconfirmed.png")
527
528             if value is not None:
529                 v_str = format_satoshis(value, True, self.wallet.num_zeros)
530             else:
531                 v_str = '--'
532
533             balance_str = format_satoshis(balance, False, self.wallet.num_zeros)
534             
535             if tx_hash:
536                 label, is_default_label = self.wallet.get_label(tx_hash)
537             else:
538                 label = _('Pruned transaction outputs')
539                 is_default_label = False
540
541             item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
542             item.setFont(2, QFont(MONOSPACE_FONT))
543             item.setFont(3, QFont(MONOSPACE_FONT))
544             item.setFont(4, QFont(MONOSPACE_FONT))
545             if tx_hash:
546                 item.setToolTip(0, tx_hash)
547             if is_default_label:
548                 item.setForeground(2, QBrush(QColor('grey')))
549
550             item.setIcon(0, icon)
551             self.history_list.insertTopLevelItem(0,item)
552             
553
554         self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
555
556
557     def create_send_tab(self):
558         w = QWidget()
559
560         grid = QGridLayout()
561         grid.setSpacing(8)
562         grid.setColumnMinimumWidth(3,300)
563         grid.setColumnStretch(5,1)
564
565         self.payto_e = QLineEdit()
566         grid.addWidget(QLabel(_('Pay to')), 1, 0)
567         grid.addWidget(self.payto_e, 1, 1, 1, 3)
568         
569         def fill_from_qr():
570             qrcode = qrscanner.scan_qr()
571             if 'address' in qrcode:
572                 self.payto_e.setText(qrcode['address'])
573             if 'amount' in qrcode:
574                 self.amount_e.setText(str(qrcode['amount']))
575             if 'label' in qrcode:
576                 self.message_e.setText(qrcode['label'])
577             if 'message' in qrcode:
578                 self.message_e.setText("%s (%s)" % (self.message_e.text(), qrcode['message']))
579                 
580
581         if qrscanner.is_available():
582             b = QPushButton(_("Scan QR code"))
583             b.clicked.connect(fill_from_qr)
584             grid.addWidget(b, 1, 5)
585     
586         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)
587
588         completer = QCompleter()
589         completer.setCaseSensitivity(False)
590         self.payto_e.setCompleter(completer)
591         completer.setModel(self.completions)
592
593         self.message_e = QLineEdit()
594         grid.addWidget(QLabel(_('Description')), 2, 0)
595         grid.addWidget(self.message_e, 2, 1, 1, 3)
596         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)
597
598         self.amount_e = QLineEdit()
599         grid.addWidget(QLabel(_('Amount')), 3, 0)
600         grid.addWidget(self.amount_e, 3, 1, 1, 2)
601         grid.addWidget(HelpButton(
602                 _('Amount to be sent.') + '\n\n' \
603                     + _('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)
604         
605         self.fee_e = QLineEdit()
606         grid.addWidget(QLabel(_('Fee')), 4, 0)
607         grid.addWidget(self.fee_e, 4, 1, 1, 2) 
608         grid.addWidget(HelpButton(
609                 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
610                     + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
611                     + _('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)
612         
613         b = EnterButton(_("Send"), self.do_send)
614         grid.addWidget(b, 6, 1)
615
616         b = EnterButton(_("Clear"),self.do_clear)
617         grid.addWidget(b, 6, 2)
618
619         self.payto_sig = QLabel('')
620         grid.addWidget(self.payto_sig, 7, 0, 1, 4)
621
622         QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
623         QShortcut(QKeySequence("Down"), w, w.focusNextChild)
624         w.setLayout(grid) 
625
626         w2 = QWidget()
627         vbox = QVBoxLayout()
628         vbox.addWidget(w)
629         vbox.addStretch(1)
630         w2.setLayout(vbox)
631
632         def entry_changed( is_fee ):
633             self.funds_error = False
634             amount = numbify(self.amount_e)
635             fee = numbify(self.fee_e)
636             if not is_fee: fee = None
637             if amount is None:
638                 return
639             inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
640             if not is_fee:
641                 self.fee_e.setText( str( Decimal( fee ) / 100000000 ) )
642             if inputs:
643                 palette = QPalette()
644                 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
645             else:
646                 palette = QPalette()
647                 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
648                 self.funds_error = True
649             self.amount_e.setPalette(palette)
650             self.fee_e.setPalette(palette)
651
652         self.amount_e.textChanged.connect(lambda: entry_changed(False) )
653         self.fee_e.textChanged.connect(lambda: entry_changed(True) )
654
655         return w2
656
657
658     def update_completions(self):
659         l = []
660         for addr,label in self.wallet.labels.items():
661             if addr in self.wallet.addressbook:
662                 l.append( label + '  <' + addr + '>')
663         l = l + self.wallet.aliases.keys()
664
665         self.completions.setStringList(l)
666
667
668
669     def do_send(self):
670
671         label = unicode( self.message_e.text() )
672         r = unicode( self.payto_e.text() )
673         r = r.strip()
674
675         # alias
676         m1 = re.match(ALIAS_REGEXP, r)
677         # label or alias, with address in brackets
678         m2 = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
679         
680         if m1:
681             to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
682             if not to_address:
683                 return
684         elif m2:
685             to_address = m2.group(2)
686         else:
687             to_address = r
688
689         if not self.wallet.is_valid(to_address):
690             QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
691             return
692
693         try:
694             amount = int( Decimal( unicode( self.amount_e.text())) * 100000000 )
695         except:
696             QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
697             return
698         try:
699             fee = int( Decimal( unicode( self.fee_e.text())) * 100000000 )
700         except:
701             QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
702             return
703
704         if self.wallet.use_encryption:
705             password = self.password_dialog()
706             if not password:
707                 return
708         else:
709             password = None
710
711         try:
712             tx = self.wallet.mktx( to_address, amount, label, password, fee)
713         except BaseException, e:
714             self.show_message(str(e))
715             return
716             
717         h = self.wallet.send_tx(tx)
718         waiting_dialog(lambda: False if self.wallet.tx_event.isSet() else _("Please wait..."))
719         status, msg = self.wallet.receive_tx( h )
720
721         if status:
722             QMessageBox.information(self, '', _('Payment sent.')+'\n'+msg, _('OK'))
723             self.do_clear()
724             self.update_contacts_tab()
725         else:
726             QMessageBox.warning(self, _('Error'), msg, _('OK'))
727
728
729     def set_url(self, url):
730         payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question)
731         self.tabs.setCurrentIndex(1)
732         label = self.wallet.labels.get(payto)
733         m_addr = label + '  <'+ payto+'>' if label else payto
734         self.payto_e.setText(m_addr)
735
736         self.message_e.setText(message)
737         self.amount_e.setText(amount)
738         if identity:
739             self.set_frozen(self.payto_e,True)
740             self.set_frozen(self.amount_e,True)
741             self.set_frozen(self.message_e,True)
742             self.payto_sig.setText( '      The bitcoin URI was signed by ' + identity )
743         else:
744             self.payto_sig.setVisible(False)
745
746     def do_clear(self):
747         self.payto_sig.setVisible(False)
748         for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
749             e.setText('')
750             self.set_frozen(e,False)
751
752     def set_frozen(self,entry,frozen):
753         if frozen:
754             entry.setReadOnly(True)
755             entry.setFrame(False)
756             palette = QPalette()
757             palette.setColor(entry.backgroundRole(), QColor('lightgray'))
758             entry.setPalette(palette)
759         else:
760             entry.setReadOnly(False)
761             entry.setFrame(True)
762             palette = QPalette()
763             palette.setColor(entry.backgroundRole(), QColor('white'))
764             entry.setPalette(palette)
765
766
767     def toggle_freeze(self,addr):
768         if not addr: return
769         if addr in self.wallet.frozen_addresses:
770             self.wallet.unfreeze(addr)
771         else:
772             self.wallet.freeze(addr)
773         self.update_receive_tab()
774
775     def toggle_priority(self,addr):
776         if not addr: return
777         if addr in self.wallet.prioritized_addresses:
778             self.wallet.unprioritize(addr)
779         else:
780             self.wallet.prioritize(addr)
781         self.update_receive_tab()
782
783
784     def create_list_tab(self, headers):
785         "generic tab creation method"
786         l = MyTreeWidget(self)
787         l.setColumnCount( len(headers) )
788         l.setHeaderLabels( headers )
789
790         w = QWidget()
791         vbox = QVBoxLayout()
792         w.setLayout(vbox)
793
794         vbox.setMargin(0)
795         vbox.setSpacing(0)
796         vbox.addWidget(l)
797         buttons = QWidget()
798         vbox.addWidget(buttons)
799
800         hbox = QHBoxLayout()
801         hbox.setMargin(0)
802         hbox.setSpacing(0)
803         buttons.setLayout(hbox)
804
805         return l,w,hbox
806
807
808     def create_receive_tab(self):
809         l,w,hbox = self.create_list_tab([_('Flags'), _('Address'), _('Label'), _('Requested'), _('Balance'), _('Tx')])
810         l.setContextMenuPolicy(Qt.CustomContextMenu)
811         l.customContextMenuRequested.connect(self.create_receive_menu)
812         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,1,2))
813         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,1,2))
814         self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.recv_changed(a))
815         self.receive_list = l
816         self.receive_buttons_hbox = hbox
817         self.qr_button = EnterButton(self.qr_button_text(), self.toggle_QR_window)
818         hbox.addWidget(self.qr_button)
819         self.print_button = EnterButton(_("Print QR"), self.print_qr)
820         self.print_button.setHidden(True)
821         hbox.addWidget(self.print_button)
822
823         self.details_button = EnterButton(self.details_button_text(), self.toggle_detailed_view)
824         hbox.addWidget(self.details_button)
825         hbox.addStretch(1)
826         return w
827
828     def print_qr(self):
829         if self.qr_window:
830             self.qr_window.do_save()
831             self.show_message(_("QR code saved to file") + " " + self.qr_window.filename)
832
833
834     def request_amount_dialog(self, address):
835         # pick the first unused address
836         # print "please wait" in qr window
837         # enter amount in usd
838
839         d = QDialog()
840         d.setWindowTitle('Request payment')
841         d.setModal(1)
842
843         vbox = QVBoxLayout()
844         vbox.addWidget(QLabel(address))
845
846         grid = QGridLayout()
847         grid.setSpacing(8)
848         vbox.addLayout(grid)
849
850         try:
851             index = self.wallet.addresses.index(address)
852         except:
853             return
854
855         label = self.wallet.labels.get(address,'invoice %04d'%(index+1))
856         grid.addWidget(QLabel(_('Label')), 0, 0)
857         label_e = QLineEdit()
858         label_e.setText(label)
859         grid.addWidget(label_e, 0, 1)
860
861         amount_e = QLineEdit()
862         amount_e.textChanged.connect(lambda: numbify(amount_e))
863
864         grid.addWidget(QLabel(_('Amount')), 1, 0)
865         grid.addWidget(amount_e, 1, 1, 1, 3)
866
867         vbox.addLayout(ok_cancel_buttons(d))
868         d.setLayout(vbox)
869
870         amount_e.setFocus()
871         if not d.exec_(): return
872         try:
873             amount = int( Decimal( unicode( amount_e.text())) * 100000000 )
874         except:
875             return
876         self.wallet.requested_amounts[address] = amount
877
878         label = unicode(label_e.text())
879         if label:
880             self.wallet.labels[address] = label
881
882         self.update_receive_item(self.receive_list.currentItem())
883         if self.qr_window:
884             self.qr_window.set_content( address, label, amount )
885         
886         
887     def details_button_text(self):
888         return _('Hide details') if self.detailed_view else _('Show details')
889
890     def qr_button_text(self):
891         return _('Hide QR') if self.qr_window and self.qr_window.isVisible() else _('Show QR')
892
893
894     def toggle_detailed_view(self):
895         self.detailed_view = not self.detailed_view
896         self.config.set_key('qt_detailed_view', self.detailed_view, True)
897
898         self.details_button.setText(self.details_button_text())
899         self.wallet.save()
900         self.update_receive_tab()
901         self.update_contacts_tab()
902
903
904     def create_contacts_tab(self):
905         l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
906         l.setContextMenuPolicy(Qt.CustomContextMenu)
907         l.customContextMenuRequested.connect(self.create_contact_menu)
908         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
909         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
910         self.contacts_list = l
911         self.contacts_buttons_hbox = hbox
912         hbox.addWidget(EnterButton(_("New"), self.new_contact_dialog))
913         hbox.addStretch(1)
914         return w
915
916
917     def create_receive_menu(self, position):
918         # fixme: this function apparently has a side effect.
919         # if it is not called the menu pops up several times
920         #self.receive_list.selectedIndexes() 
921
922         item = self.receive_list.itemAt(position)
923         if not item: return
924         addr = unicode(item.text(1))
925         menu = QMenu()
926         menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
927         if self.qr_window and self.qr_window.isVisible():
928             menu.addAction(_("Request amount"), lambda: self.request_amount_dialog(addr))
929         menu.addAction(_("Edit label"), lambda: self.edit_label(True))
930         menu.addAction(_("Sign message"), lambda: self.sign_message(addr))
931
932         t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
933         menu.addAction(t, lambda: self.toggle_freeze(addr))
934         t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
935         menu.addAction(t, lambda: self.toggle_priority(addr))
936         menu.exec_(self.receive_list.viewport().mapToGlobal(position))
937
938
939     def payto(self, x, is_alias):
940         if not x: return
941         if is_alias:
942             label = x
943             m_addr = label
944         else:
945             addr = x
946             label = self.wallet.labels.get(addr)
947             m_addr = label + '  <' + addr + '>' if label else addr
948         self.tabs.setCurrentIndex(1)
949         self.payto_e.setText(m_addr)
950         self.amount_e.setFocus()
951
952     def delete_contact(self, x, is_alias):
953         if self.question("Do you want to remove %s from your list of contacts?"%x):
954             if not is_alias and x in self.wallet.addressbook:
955                 self.wallet.addressbook.remove(x)
956                 if x in self.wallet.labels.keys():
957                     self.wallet.labels.pop(x)
958             elif is_alias and x in self.wallet.aliases:
959                 self.wallet.aliases.pop(x)
960             self.update_history_tab()
961             self.update_contacts_tab()
962             self.update_completions()
963
964     def create_contact_menu(self, position):
965         # fixme: this function apparently has a side effect.
966         # if it is not called the menu pops up several times
967         #self.contacts_list.selectedIndexes() 
968
969         item = self.contacts_list.itemAt(position)
970         if not item: return
971         addr = unicode(item.text(0))
972         label = unicode(item.text(1))
973         is_alias = label in self.wallet.aliases.keys()
974         x = label if is_alias else addr
975         menu = QMenu()
976         menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
977         menu.addAction(_("Pay to"), lambda: self.payto(x, is_alias))
978         menu.addAction(_("View QR code"),lambda: self.show_address_qrcode(addr))
979         if not is_alias:
980             menu.addAction(_("Edit label"), lambda: self.edit_label(False))
981         else:
982             menu.addAction(_("View alias details"), lambda: self.show_contact_details(label))
983         menu.addAction(_("Delete"), lambda: self.delete_contact(x,is_alias))
984         menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
985
986
987     def update_receive_item(self, item):
988         address = str( item.data(1,0).toString() )
989
990         flags = self.wallet.get_address_flags(address)
991         item.setData(0,0,flags)
992
993         label = self.wallet.labels.get(address,'')
994         item.setData(2,0,label)
995
996         amount = self.wallet.requested_amounts.get(address,None)
997         amount_str = format_satoshis( amount, False, self.wallet.num_zeros ) if amount is not None  else "--"
998         item.setData(3,0,amount_str)
999         
1000         c, u = self.wallet.get_addr_balance(address)
1001         balance = format_satoshis( c + u, False, self.wallet.num_zeros )
1002         item.setData(4,0,balance)
1003
1004         if address in self.wallet.frozen_addresses: 
1005             item.setBackgroundColor(1, QColor('lightblue'))
1006         elif address in self.wallet.prioritized_addresses: 
1007             item.setBackgroundColor(1, QColor('lightgreen'))
1008         
1009
1010     def update_receive_tab(self):
1011         l = self.receive_list
1012         
1013         l.clear()
1014         l.setColumnHidden(0, not self.detailed_view)
1015         l.setColumnHidden(3, self.qr_window is None or not self.qr_window.isVisible())
1016         l.setColumnHidden(4, not self.detailed_view)
1017         l.setColumnHidden(5, not self.detailed_view)
1018         l.setColumnWidth(0, 50)
1019         l.setColumnWidth(1, 310) 
1020         l.setColumnWidth(2, 200)
1021         l.setColumnWidth(3, 130)
1022         l.setColumnWidth(4, 130)
1023         l.setColumnWidth(5, 10)
1024
1025         gap = 0
1026         is_red = False
1027         for address in self.wallet.all_addresses():
1028
1029             if self.wallet.is_change(address) and not self.detailed_view:
1030                 continue
1031
1032             n = 0 
1033             h = self.wallet.history.get(address,[])
1034
1035             if h != ['*']: 
1036                 for tx_hash, tx_height in h:
1037                     tx = self.wallet.transactions.get(tx_hash)
1038                     if tx: n += 1
1039                 num_tx = "%d "%n
1040             else:
1041                 n = -1
1042                 num_tx = "*"
1043
1044             if n==0:
1045                 if address in self.wallet.addresses:
1046                     gap += 1
1047                     if gap > self.wallet.gap_limit:
1048                         is_red = True
1049             else:
1050                 if address in self.wallet.addresses:
1051                     gap = 0
1052
1053             item = QTreeWidgetItem( [ '', address, '', '', '', num_tx] )
1054             item.setFont(0, QFont(MONOSPACE_FONT))
1055             item.setFont(1, QFont(MONOSPACE_FONT))
1056             item.setFont(3, QFont(MONOSPACE_FONT))
1057             self.update_receive_item(item)
1058             if is_red and address in self.wallet.addresses:
1059                 item.setBackgroundColor(1, QColor('red'))
1060             l.addTopLevelItem(item)
1061
1062         # we use column 1 because column 0 may be hidden
1063         l.setCurrentItem(l.topLevelItem(0),1)
1064
1065     def show_contact_details(self, m):
1066         a = self.wallet.aliases.get(m)
1067         if a:
1068             if a[0] in self.wallet.authorities.keys():
1069                 s = self.wallet.authorities.get(a[0])
1070             else:
1071                 s = "self-signed"
1072             msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0]
1073             QMessageBox.information(self, 'Alias', msg, 'OK')
1074
1075     def update_contacts_tab(self):
1076
1077         l = self.contacts_list
1078         l.clear()
1079         l.setColumnHidden(2, not self.detailed_view)
1080         l.setColumnWidth(0, 350) 
1081         l.setColumnWidth(1, 330)
1082         l.setColumnWidth(2, 100) 
1083
1084         alias_targets = []
1085         for alias, v in self.wallet.aliases.items():
1086             s, target = v
1087             alias_targets.append(target)
1088             item = QTreeWidgetItem( [ target, alias, '-'] )
1089             item.setBackgroundColor(0, QColor('lightgray'))
1090             l.addTopLevelItem(item)
1091             
1092         for address in self.wallet.addressbook:
1093             if address in alias_targets: continue
1094             label = self.wallet.labels.get(address,'')
1095             n = 0 
1096             for item in self.wallet.transactions.values():
1097                 if address in item['outputs'] : n=n+1
1098             tx = "%d"%n
1099             item = QTreeWidgetItem( [ address, label, tx] )
1100             item.setFont(0, QFont(MONOSPACE_FONT))
1101             l.addTopLevelItem(item)
1102
1103         l.setCurrentItem(l.topLevelItem(0))
1104
1105     def create_wall_tab(self):
1106         self.textbox = textbox = QTextEdit(self)
1107         textbox.setFont(QFont(MONOSPACE_FONT))
1108         textbox.setReadOnly(True)
1109         return textbox
1110
1111     def create_status_bar(self):
1112         sb = QStatusBar()
1113         sb.setFixedHeight(35)
1114         if self.wallet.seed:
1115             sb.addPermanentWidget( StatusBarButton( QIcon(":icons/lock.png"), "Password", lambda: self.change_password_dialog(self.wallet, self) ) )
1116         sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), "Preferences", self.settings_dialog ) )
1117         if self.wallet.seed:
1118             sb.addPermanentWidget( StatusBarButton( QIcon(":icons/seed.png"), "Seed", lambda: self.show_seed_dialog(self.wallet, self) ) )
1119         self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), "Network", lambda: self.network_dialog(self.wallet, self) ) 
1120         sb.addPermanentWidget( self.status_button )
1121         self.setStatusBar(sb)
1122
1123     def new_contact_dialog(self):
1124         text, ok = QInputDialog.getText(self, _('New Contact'), _('Address') + ':')
1125         address = unicode(text)
1126         if ok:
1127             if self.wallet.is_valid(address):
1128                 self.wallet.addressbook.append(address)
1129                 self.wallet.save()
1130                 self.update_contacts_tab()
1131                 self.update_history_tab()
1132                 self.update_completions()
1133             else:
1134                 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1135
1136     @staticmethod
1137     def show_seed_dialog(wallet, parent=None):
1138         if not wallet.seed:
1139             QMessageBox.information(parent, _('Message'),
1140                                     _('No seed'), _('OK'))
1141             return
1142
1143         if wallet.use_encryption:
1144             password = parent.password_dialog()
1145             if not password:
1146                 return
1147         else:
1148             password = None
1149             
1150         try:
1151             seed = wallet.pw_decode(wallet.seed, password)
1152         except:
1153             QMessageBox.warning(parent, _('Error'),
1154                                 _('Incorrect Password'), _('OK'))
1155             return
1156
1157         dialog = QDialog(None)
1158         dialog.setModal(1)
1159         dialog.setWindowTitle("Electrum")
1160
1161         brainwallet = ' '.join(mnemonic.mn_encode(seed))
1162
1163         msg =   _("Your wallet generation seed is") +":<p>\"" + brainwallet + "\"<p>" \
1164               + _("Please write down or memorize these 12 words (order is important).") + " " \
1165               + _("This seed will allow you to recover your wallet in case of computer failure.") + "<p>" \
1166               + _("WARNING: Never disclose your seed. Never type it on a website.") + "<p>"
1167
1168         main_text = QLabel(msg)
1169         main_text.setWordWrap(True)
1170
1171         logo = QLabel()
1172         logo.setPixmap(QPixmap(":icons/seed.png").scaledToWidth(56))
1173
1174         if parent:
1175             app = parent.app
1176         else:
1177             app = QApplication
1178
1179         copy_function = lambda: app.clipboard().setText(brainwallet)
1180         copy_button = QPushButton(_("Copy to Clipboard"))
1181         copy_button.clicked.connect(copy_function)
1182
1183         show_qr_function = lambda: ElectrumWindow.show_seed_qrcode(seed)
1184         qr_button = QPushButton(_("View as QR Code"))
1185         qr_button.clicked.connect(show_qr_function)
1186
1187         ok_button = QPushButton(_("OK"))
1188         ok_button.setDefault(True)
1189         ok_button.clicked.connect(dialog.accept)
1190
1191         main_layout = QGridLayout()
1192         main_layout.addWidget(logo, 0, 0)
1193         main_layout.addWidget(main_text, 0, 1, 1, -1)
1194         main_layout.addWidget(copy_button, 1, 1)
1195         main_layout.addWidget(qr_button, 1, 2)
1196         main_layout.addWidget(ok_button, 1, 3)
1197         dialog.setLayout(main_layout)
1198
1199         dialog.exec_()
1200
1201     @staticmethod
1202     def show_seed_qrcode(seed):
1203         if not seed: return
1204         d = QDialog(None)
1205         d.setModal(1)
1206         d.setWindowTitle(_("Seed"))
1207         d.setMinimumSize(270, 300)
1208         vbox = QVBoxLayout()
1209         vbox.addWidget(QRCodeWidget(seed))
1210         hbox = QHBoxLayout()
1211         hbox.addStretch(1)
1212         b = QPushButton(_("OK"))
1213         hbox.addWidget(b)
1214         b.clicked.connect(d.accept)
1215
1216         vbox.addLayout(hbox)
1217         d.setLayout(vbox)
1218         d.exec_()
1219
1220     def sign_message(self,address):
1221         if not address: return
1222         d = QDialog(self)
1223         d.setModal(1)
1224         d.setWindowTitle('Sign Message')
1225         d.setMinimumSize(270, 350)
1226
1227         tab_widget = QTabWidget()
1228         tab = QWidget()
1229         layout = QGridLayout(tab)
1230
1231         sign_address = QLineEdit()
1232         sign_address.setText(address)
1233         layout.addWidget(QLabel(_('Address')), 1, 0)
1234         layout.addWidget(sign_address, 1, 1)
1235
1236         sign_message = QTextEdit()
1237         layout.addWidget(QLabel(_('Message')), 2, 0)
1238         layout.addWidget(sign_message, 2, 1, 2, 1)
1239
1240         sign_signature = QLineEdit()
1241         layout.addWidget(QLabel(_('Signature')), 3, 0)
1242         layout.addWidget(sign_signature, 3, 1)
1243
1244         def do_sign():
1245             if self.wallet.use_encryption:
1246                 password = self.password_dialog()
1247                 if not password:
1248                     return
1249             else:
1250                 password = None
1251
1252             try:
1253                 signature = self.wallet.sign_message(sign_address.text(), sign_message.toPlainText(), password)
1254                 sign_signature.setText(signature)
1255             except BaseException, e:
1256                 self.show_message(str(e))
1257                 return
1258
1259         hbox = QHBoxLayout()
1260         b = QPushButton(_("Sign"))
1261         hbox.addWidget(b)
1262         b.clicked.connect(do_sign)
1263         b = QPushButton(_("Close"))
1264         b.clicked.connect(d.accept)
1265         hbox.addWidget(b)
1266         layout.addLayout(hbox, 4, 1)
1267         tab_widget.addTab(tab, "Sign")
1268
1269
1270         tab = QWidget()
1271         layout = QGridLayout(tab)
1272
1273         verify_address = QLineEdit()
1274         layout.addWidget(QLabel(_('Address')), 1, 0)
1275         layout.addWidget(verify_address, 1, 1)
1276
1277         verify_message = QTextEdit()
1278         layout.addWidget(QLabel(_('Message')), 2, 0)
1279         layout.addWidget(verify_message, 2, 1, 2, 1)
1280
1281         verify_signature = QLineEdit()
1282         layout.addWidget(QLabel(_('Signature')), 3, 0)
1283         layout.addWidget(verify_signature, 3, 1)
1284
1285         def do_verify():
1286             try:
1287                 self.wallet.verify_message(verify_address.text(), verify_signature.text(), verify_message.toPlainText())
1288                 self.show_message("Signature verified")
1289             except BaseException, e:
1290                 self.show_message(str(e))
1291                 return
1292
1293         hbox = QHBoxLayout()
1294         b = QPushButton(_("Verify"))
1295         b.clicked.connect(do_verify)
1296         hbox.addWidget(b)
1297         b = QPushButton(_("Close"))
1298         b.clicked.connect(d.accept)
1299         hbox.addWidget(b)
1300         layout.addLayout(hbox, 4, 1)
1301         tab_widget.addTab(tab, "Verify")
1302
1303         vbox = QVBoxLayout()
1304         vbox.addWidget(tab_widget)
1305         d.setLayout(vbox)
1306         d.exec_()
1307
1308         
1309     def toggle_QR_window(self):
1310         if not self.qr_window:
1311             self.qr_window = QR_Window()
1312             self.qr_window.setVisible(True)
1313             #print self.qr_window.isVisible()
1314             self.qr_window_geometry = self.qr_window.geometry()
1315             item = self.receive_list.currentItem()
1316             if item:
1317                 address = str(item.text(1))
1318                 label = self.wallet.labels.get(address)
1319                 amount = self.wallet.requested_amounts.get(address)
1320                 self.qr_window.set_content( address, label, amount )
1321                 self.update_receive_tab()
1322         else:
1323             if self.qr_window.isVisible():
1324                 self.qr_window_geometry = self.qr_window.geometry()
1325                 self.qr_window.setVisible(False)
1326             else:
1327                 self.qr_window.setVisible(True)
1328                 self.qr_window.setGeometry(self.qr_window_geometry)
1329
1330         self.qr_button.setText(self.qr_button_text())
1331         self.print_button.setHidden(self.qr_window is None or not self.qr_window.isVisible())
1332         self.receive_list.setColumnHidden(3, self.qr_window is None or not self.qr_window.isVisible())
1333         self.receive_list.setColumnWidth(2, 200)
1334
1335
1336     def question(self, msg):
1337         return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1338
1339     def show_message(self, msg):
1340         QMessageBox.information(self, _('Message'), msg, _('OK'))
1341
1342     def password_dialog(self ):
1343         d = QDialog(self)
1344         d.setModal(1)
1345
1346         pw = QLineEdit()
1347         pw.setEchoMode(2)
1348
1349         vbox = QVBoxLayout()
1350         msg = _('Please enter your password')
1351         vbox.addWidget(QLabel(msg))
1352
1353         grid = QGridLayout()
1354         grid.setSpacing(8)
1355         grid.addWidget(QLabel(_('Password')), 1, 0)
1356         grid.addWidget(pw, 1, 1)
1357         vbox.addLayout(grid)
1358
1359         vbox.addLayout(ok_cancel_buttons(d))
1360         d.setLayout(vbox) 
1361
1362         if not d.exec_(): return
1363         return unicode(pw.text())
1364
1365
1366
1367
1368
1369     @staticmethod
1370     def change_password_dialog( wallet, parent=None ):
1371
1372         if not wallet.seed:
1373             QMessageBox.information(parent, _('Error'), _('No seed'), _('OK'))
1374             return
1375
1376         d = QDialog(parent)
1377         d.setModal(1)
1378
1379         pw = QLineEdit()
1380         pw.setEchoMode(2)
1381         new_pw = QLineEdit()
1382         new_pw.setEchoMode(2)
1383         conf_pw = QLineEdit()
1384         conf_pw.setEchoMode(2)
1385
1386         vbox = QVBoxLayout()
1387         if parent:
1388             msg = (_('Your wallet is encrypted. Use this dialog to change your password.')+'\n'\
1389                    +_('To disable wallet encryption, enter an empty new password.')) \
1390                    if wallet.use_encryption else _('Your wallet keys are not encrypted')
1391         else:
1392             msg = _("Please choose a password to encrypt your wallet keys.")+'\n'\
1393                   +_("Leave these fields empty if you want to disable encryption.")
1394         vbox.addWidget(QLabel(msg))
1395
1396         grid = QGridLayout()
1397         grid.setSpacing(8)
1398
1399         if wallet.use_encryption:
1400             grid.addWidget(QLabel(_('Password')), 1, 0)
1401             grid.addWidget(pw, 1, 1)
1402
1403         grid.addWidget(QLabel(_('New Password')), 2, 0)
1404         grid.addWidget(new_pw, 2, 1)
1405
1406         grid.addWidget(QLabel(_('Confirm Password')), 3, 0)
1407         grid.addWidget(conf_pw, 3, 1)
1408         vbox.addLayout(grid)
1409
1410         vbox.addLayout(ok_cancel_buttons(d))
1411         d.setLayout(vbox) 
1412
1413         if not d.exec_(): return
1414
1415         password = unicode(pw.text()) if wallet.use_encryption else None
1416         new_password = unicode(new_pw.text())
1417         new_password2 = unicode(conf_pw.text())
1418
1419         try:
1420             seed = wallet.pw_decode( wallet.seed, password)
1421         except:
1422             QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
1423             return
1424
1425         if new_password != new_password2:
1426             QMessageBox.warning(parent, _('Error'), _('Passwords do not match'), _('OK'))
1427             return
1428
1429         wallet.update_password(seed, password, new_password)
1430
1431     @staticmethod
1432     def seed_dialog(wallet, parent=None):
1433         d = QDialog(parent)
1434         d.setModal(1)
1435
1436         vbox = QVBoxLayout()
1437         msg = _("Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet.")
1438         vbox.addWidget(QLabel(msg))
1439
1440         grid = QGridLayout()
1441         grid.setSpacing(8)
1442
1443         seed_e = QLineEdit()
1444         grid.addWidget(QLabel(_('Seed or mnemonic')), 1, 0)
1445         grid.addWidget(seed_e, 1, 1)
1446
1447         gap_e = QLineEdit()
1448         gap_e.setText("5")
1449         grid.addWidget(QLabel(_('Gap limit')), 2, 0)
1450         grid.addWidget(gap_e, 2, 1)
1451         gap_e.textChanged.connect(lambda: numbify(gap_e,True))
1452         vbox.addLayout(grid)
1453
1454         vbox.addLayout(ok_cancel_buttons(d))
1455         d.setLayout(vbox) 
1456
1457         if not d.exec_(): return
1458
1459         try:
1460             gap = int(unicode(gap_e.text()))
1461         except:
1462             QMessageBox.warning(None, _('Error'), 'error', 'OK')
1463             sys.exit(0)
1464
1465         try:
1466             seed = unicode(seed_e.text())
1467             seed.decode('hex')
1468         except:
1469             print_error("Warning: Not hex, trying decode")
1470             try:
1471                 seed = mnemonic.mn_decode( seed.split(' ') )
1472             except:
1473                 QMessageBox.warning(None, _('Error'), _('I cannot decode this'), _('OK'))
1474                 sys.exit(0)
1475         if not seed:
1476             QMessageBox.warning(None, _('Error'), _('No seed'), 'OK')
1477             sys.exit(0)
1478         
1479         wallet.seed = str(seed)
1480         #print repr(wallet.seed)
1481         wallet.gap_limit = gap
1482         return True
1483
1484
1485
1486     def settings_dialog(self):
1487         d = QDialog(self)
1488         d.setModal(1)
1489         vbox = QVBoxLayout()
1490         msg = _('Here are the settings of your wallet.') + '\n'\
1491               + _('For more explanations, click on the help buttons next to each field.')
1492
1493         label = QLabel(msg)
1494         label.setFixedWidth(250)
1495         label.setWordWrap(True)
1496         label.setAlignment(Qt.AlignJustify)
1497         vbox.addWidget(label)
1498
1499         grid = QGridLayout()
1500         grid.setSpacing(8)
1501         vbox.addLayout(grid)
1502
1503         fee_label = QLabel(_('Transaction fee'))
1504         grid.addWidget(fee_label, 2, 0)
1505         fee_e = QLineEdit()
1506         fee_e.setText("%s"% str( Decimal( self.wallet.fee)/100000000 ) )
1507         grid.addWidget(fee_e, 2, 1)
1508         msg = _('Fee per transaction input. Transactions involving multiple inputs tend to require a higher fee.') + ' ' \
1509             + _('Recommended value') + ': 0.001'
1510         grid.addWidget(HelpButton(msg), 2, 2)
1511         fee_e.textChanged.connect(lambda: numbify(fee_e,False))
1512         if not self.config.is_modifiable('fee'):
1513             for w in [fee_e, fee_label]: w.setEnabled(False)
1514
1515         nz_label = QLabel(_('Display zeros'))
1516         grid.addWidget(nz_label, 3, 0)
1517         nz_e = QLineEdit()
1518         nz_e.setText("%d"% self.wallet.num_zeros)
1519         grid.addWidget(nz_e, 3, 1)
1520         msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1521         grid.addWidget(HelpButton(msg), 3, 2)
1522         nz_e.textChanged.connect(lambda: numbify(nz_e,True))
1523         if not self.config.is_modifiable('num_zeros'):
1524             for w in [nz_e, nz_label]: w.setEnabled(False)
1525
1526         usechange_cb = QCheckBox(_('Use change addresses'))
1527         grid.addWidget(usechange_cb, 5, 0)
1528         usechange_cb.setChecked(self.wallet.use_change)
1529         grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions. ')), 5, 2)
1530         if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
1531
1532         gap_label = QLabel(_('Gap limit'))
1533         grid.addWidget(gap_label, 6, 0)
1534         gap_e = QLineEdit()
1535         gap_e.setText("%d"% self.wallet.gap_limit)
1536         grid.addWidget(gap_e, 6, 1)
1537         msg =  _('The gap limit is the maximal number of contiguous unused addresses in your sequence of receiving addresses.') + '\n' \
1538               + _('You may increase it if you need more receiving addresses.') + '\n\n' \
1539               + _('Your current gap limit is') + ': %d'%self.wallet.gap_limit + '\n' \
1540               + _('Given the current status of your address sequence, the minimum gap limit you can use is: ') + '%d'%self.wallet.min_acceptable_gap() + '\n\n' \
1541               + _('Warning') + ': ' \
1542               + _('The gap limit parameter must be provided in order to recover your wallet from seed.') + ' ' \
1543               + _('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' 
1544         grid.addWidget(HelpButton(msg), 6, 2)
1545         gap_e.textChanged.connect(lambda: numbify(nz_e,True))
1546         if not self.config.is_modifiable('gap_limit'):
1547             for w in [gap_e, gap_label]: w.setEnabled(False)
1548         
1549         gui_label=QLabel(_('Default GUI') + ':')
1550         grid.addWidget(gui_label , 7, 0)
1551         gui_combo = QComboBox()
1552         gui_combo.addItems(['Lite', 'Classic', 'Gtk', 'Text'])
1553         index = gui_combo.findText(self.config.get("gui","classic").capitalize())
1554         if index==-1: index = 1
1555         gui_combo.setCurrentIndex(index)
1556         grid.addWidget(gui_combo, 7, 1)
1557         grid.addWidget(HelpButton(_('Select which GUI mode to use at start up. ')), 7, 2)
1558         if not self.config.is_modifiable('gui'):
1559             for w in [gui_combo, gui_label]: w.setEnabled(False)
1560
1561         vbox.addLayout(ok_cancel_buttons(d))
1562         d.setLayout(vbox) 
1563
1564         # run the dialog
1565         if not d.exec_(): return
1566
1567         fee = unicode(fee_e.text())
1568         try:
1569             fee = int( 100000000 * Decimal(fee) )
1570         except:
1571             QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
1572             return
1573
1574         if self.wallet.fee != fee:
1575             self.wallet.fee = fee
1576             self.wallet.save()
1577         
1578         nz = unicode(nz_e.text())
1579         try:
1580             nz = int( nz )
1581             if nz>8: nz=8
1582         except:
1583             QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
1584             return
1585
1586         if self.wallet.num_zeros != nz:
1587             self.wallet.num_zeros = nz
1588             self.config.set_key('num_zeros', nz, True)
1589             self.update_history_tab()
1590             self.update_receive_tab()
1591
1592         if self.wallet.use_change != usechange_cb.isChecked():
1593             self.wallet.use_change = usechange_cb.isChecked()
1594             self.config.set_key('use_change', self.wallet.use_change, True)
1595         
1596         try:
1597             n = int(gap_e.text())
1598         except:
1599             QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
1600             return
1601
1602         if self.wallet.gap_limit != n:
1603             r = self.wallet.change_gap_limit(n)
1604             if r:
1605                 self.update_receive_tab()
1606                 self.config.set_key('gap_limit', self.wallet.gap_limit, True)
1607             else:
1608                 QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
1609                     
1610         self.config.set_key("gui", str(gui_combo.currentText()).lower(), True)
1611
1612
1613
1614     @staticmethod 
1615     def network_dialog(wallet, parent=None):
1616         interface = wallet.interface
1617         if parent:
1618             if interface.is_connected:
1619                 status = _("Connected to")+" %s\n%d blocks"%(interface.host, wallet.verifier.height)
1620             else:
1621                 status = _("Not connected")
1622             server = interface.server
1623         else:
1624             import random
1625             status = _("Please choose a server.") + "\n" + _("Select 'Cancel' if you are offline.")
1626             server = interface.server
1627
1628         plist, servers_list = interface.get_servers_list()
1629
1630         d = QDialog(parent)
1631         d.setModal(1)
1632         d.setWindowTitle(_('Server'))
1633         d.setMinimumSize(375, 20)
1634
1635         vbox = QVBoxLayout()
1636         vbox.setSpacing(30)
1637
1638         hbox = QHBoxLayout()
1639         l = QLabel()
1640         l.setPixmap(QPixmap(":icons/network.png"))
1641         hbox.addStretch(10)
1642         hbox.addWidget(l)
1643         hbox.addWidget(QLabel(status))
1644         hbox.addStretch(50)
1645         vbox.addLayout(hbox)
1646
1647
1648         # grid layout
1649         grid = QGridLayout()
1650         grid.setSpacing(8)
1651         vbox.addLayout(grid)
1652
1653         # server
1654         server_protocol = QComboBox()
1655         server_host = QLineEdit()
1656         server_host.setFixedWidth(200)
1657         server_port = QLineEdit()
1658         server_port.setFixedWidth(60)
1659
1660         protocol_names = ['TCP', 'HTTP', 'TCP/SSL', 'HTTPS']
1661         protocol_letters = 'thsg'
1662         DEFAULT_PORTS = {'t':'50001', 's':'50002', 'h':'8081', 'g':'8082'}
1663         server_protocol.addItems(protocol_names)
1664
1665         grid.addWidget(QLabel(_('Server') + ':'), 0, 0)
1666         grid.addWidget(server_protocol, 0, 1)
1667         grid.addWidget(server_host, 0, 2)
1668         grid.addWidget(server_port, 0, 3)
1669
1670         def change_protocol(p):
1671             protocol = protocol_letters[p]
1672             host = unicode(server_host.text())
1673             pp = plist.get(host,DEFAULT_PORTS)
1674             if protocol not in pp.keys():
1675                 protocol = pp.keys()[0]
1676             port = pp[protocol]
1677             server_host.setText( host )
1678             server_port.setText( port )
1679
1680         server_protocol.connect(server_protocol, SIGNAL('currentIndexChanged(int)'), change_protocol)
1681         
1682         label = _('Active Servers') if wallet.interface.servers else _('Default Servers')
1683         servers_list_widget = QTreeWidget(parent)
1684         servers_list_widget.setHeaderLabels( [ label, _('Type') ] )
1685         servers_list_widget.setMaximumHeight(150)
1686         servers_list_widget.setColumnWidth(0, 240)
1687         for _host in servers_list.keys():
1688             _type = 'pruning' if servers_list[_host].get('pruning') else 'full'
1689             servers_list_widget.addTopLevelItem(QTreeWidgetItem( [ _host, _type ] ))
1690
1691         def change_server(host, protocol=None):
1692             pp = plist.get(host,DEFAULT_PORTS)
1693             if protocol:
1694                 port = pp.get(protocol)
1695                 if not port: protocol = None
1696                     
1697             if not protocol:
1698                 if 't' in pp.keys():
1699                     protocol = 't'
1700                     port = pp.get(protocol)
1701                 else:
1702                     protocol = pp.keys()[0]
1703                     port = pp.get(protocol)
1704             
1705             server_host.setText( host )
1706             server_port.setText( port )
1707             server_protocol.setCurrentIndex(protocol_letters.index(protocol))
1708
1709             if not plist: return
1710             for p in protocol_letters:
1711                 i = protocol_letters.index(p)
1712                 j = server_protocol.model().index(i,0)
1713                 if p not in pp.keys():
1714                     server_protocol.model().setData(j, QtCore.QVariant(0), QtCore.Qt.UserRole-1)
1715                 else:
1716                     server_protocol.model().setData(j, QtCore.QVariant(0,False), QtCore.Qt.UserRole-1)
1717
1718
1719         if server:
1720             host, port, protocol = server.split(':')
1721             change_server(host,protocol)
1722
1723         servers_list_widget.connect(servers_list_widget, SIGNAL('itemClicked(QTreeWidgetItem*, int)'), lambda x: change_server(unicode(x.text(0))))
1724         grid.addWidget(servers_list_widget, 1, 1, 1, 3)
1725
1726         if not wallet.config.is_modifiable('server'):
1727             for w in [server_host, server_port, server_protocol, servers_list_widget]: w.setEnabled(False)
1728
1729         # proxy setting
1730         proxy_mode = QComboBox()
1731         proxy_host = QLineEdit()
1732         proxy_host.setFixedWidth(200)
1733         proxy_port = QLineEdit()
1734         proxy_port.setFixedWidth(60)
1735         proxy_mode.addItems(['NONE', 'SOCKS4', 'SOCKS5', 'HTTP'])
1736
1737         def check_for_disable(index = False):
1738             if proxy_mode.currentText() != 'NONE':
1739                 proxy_host.setEnabled(True)
1740                 proxy_port.setEnabled(True)
1741             else:
1742                 proxy_host.setEnabled(False)
1743                 proxy_port.setEnabled(False)
1744
1745         check_for_disable()
1746         proxy_mode.connect(proxy_mode, SIGNAL('currentIndexChanged(int)'), check_for_disable)
1747
1748         if not wallet.config.is_modifiable('proxy'):
1749             for w in [proxy_host, proxy_port, proxy_mode]: w.setEnabled(False)
1750
1751         proxy_config = interface.proxy if interface.proxy else { "mode":"none", "host":"localhost", "port":"8080"}
1752         proxy_mode.setCurrentIndex(proxy_mode.findText(str(proxy_config.get("mode").upper())))
1753         proxy_host.setText(proxy_config.get("host"))
1754         proxy_port.setText(proxy_config.get("port"))
1755
1756         grid.addWidget(QLabel(_('Proxy') + ':'), 2, 0)
1757         grid.addWidget(proxy_mode, 2, 1)
1758         grid.addWidget(proxy_host, 2, 2)
1759         grid.addWidget(proxy_port, 2, 3)
1760
1761         # buttons
1762         vbox.addLayout(ok_cancel_buttons(d))
1763         d.setLayout(vbox) 
1764
1765         if not d.exec_(): return
1766
1767         server = unicode( server_host.text() ) + ':' + unicode( server_port.text() ) + ':' + (protocol_letters[server_protocol.currentIndex()])
1768         if proxy_mode.currentText() != 'NONE':
1769             proxy = { u'mode':unicode(proxy_mode.currentText()).lower(), u'host':unicode(proxy_host.text()), u'port':unicode(proxy_port.text()) }
1770         else:
1771             proxy = None
1772
1773         wallet.config.set_key("proxy", proxy, True)
1774         wallet.config.set_key("server", server, True)
1775         interface.set_server(server, proxy)
1776                 
1777         return True
1778
1779     def closeEvent(self, event):
1780         g = self.geometry()
1781         self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
1782         event.accept()
1783
1784
1785 class ElectrumGui:
1786
1787     def __init__(self, wallet, config, app=None):
1788         self.wallet = wallet
1789         self.config = config
1790         if app is None:
1791             self.app = QApplication(sys.argv)
1792
1793
1794     def restore_or_create(self):
1795         msg = _("Wallet file not found.")+"\n"+_("Do you want to create a new wallet, or to restore an existing one?")
1796         r = QMessageBox.question(None, _('Message'), msg, _('Create'), _('Restore'), _('Cancel'), 0, 2)
1797         if r==2: return None
1798         return 'restore' if r==1 else 'create'
1799
1800     def seed_dialog(self):
1801         return ElectrumWindow.seed_dialog( self.wallet )
1802
1803     def network_dialog(self):
1804         return ElectrumWindow.network_dialog( self.wallet, parent=None )
1805         
1806
1807     def show_seed(self):
1808         ElectrumWindow.show_seed_dialog(self.wallet)
1809
1810
1811     def password_dialog(self):
1812         ElectrumWindow.change_password_dialog(self.wallet)
1813
1814
1815     def restore_wallet(self):
1816         wallet = self.wallet
1817         # wait until we are connected, because the user might have selected another server
1818         if not wallet.interface.is_connected:
1819             waiting = lambda: False if wallet.interface.is_connected else "connecting...\n"
1820             waiting_dialog(waiting)
1821
1822         waiting = lambda: False if wallet.is_up_to_date() else "Please wait...\nAddresses generated: %d\nKilobytes received: %.1f"\
1823             %(len(wallet.all_addresses()), wallet.interface.bytes_received/1024.)
1824
1825         wallet.set_up_to_date(False)
1826         wallet.interface.poke('synchronizer')
1827         waiting_dialog(waiting)
1828         if wallet.is_found():
1829             print_error( "Recovery successful" )
1830         else:
1831             QMessageBox.information(None, _('Error'), _("No transactions found for this seed"), _('OK'))
1832
1833         return True
1834
1835     def main(self,url):
1836         s = Timer()
1837         s.start()
1838         w = ElectrumWindow(self.wallet, self.config)
1839         if url: w.set_url(url)
1840         w.app = self.app
1841         w.connect_slots(s)
1842         w.update_wallet()
1843         w.show()
1844
1845         self.app.exec_()