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