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