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