wallet.delete_contact method
[electrum-nvc.git] / gui / gui_classic.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, threading
20 from i18n import _, set_language
21 from electrum.util import print_error, print_msg
22 import os.path, json, ast, traceback
23
24
25 try:
26     import PyQt4
27 except:
28     sys.exit("Error: Could not import PyQt4 on Linux systems, you may try 'sudo apt-get install python-qt4'")
29
30 from PyQt4.QtGui import *
31 from PyQt4.QtCore import *
32 import PyQt4.QtCore as QtCore
33
34 from electrum.bitcoin import MIN_RELAY_TX_FEE
35
36 try:
37     import icons_rc
38 except:
39     sys.exit("Error: Could not import icons_rc.py, please generate it with: 'pyrcc4 icons.qrc -o gui/icons_rc.py'")
40
41 from electrum.wallet import format_satoshis
42 from electrum.bitcoin import Transaction, is_valid
43 from electrum import mnemonic
44 from electrum import util, bitcoin, commands
45
46 import bmp, pyqrnative
47 import exchange_rate
48
49 from amountedit import AmountEdit
50 from network_dialog import NetworkDialog
51 from qrcodewidget import QRCodeWidget
52
53 from decimal import Decimal
54
55 import platform
56 import httplib
57 import socket
58 import webbrowser
59 import csv
60
61 if platform.system() == 'Windows':
62     MONOSPACE_FONT = 'Lucida Console'
63 elif platform.system() == 'Darwin':
64     MONOSPACE_FONT = 'Monaco'
65 else:
66     MONOSPACE_FONT = 'monospace'
67
68 from electrum import ELECTRUM_VERSION
69 import re
70
71 from qt_util import *
72
73 class UpdateLabel(QLabel):
74     def __init__(self, config, parent=None):
75         QLabel.__init__(self, parent)
76         self.new_version = False
77
78         try:
79             con = httplib.HTTPConnection('electrum.org', 80, timeout=5)
80             con.request("GET", "/version")
81             res = con.getresponse()
82         except socket.error as msg:
83             print_error("Could not retrieve version information")
84             return
85             
86         if res.status == 200:
87             self.latest_version = res.read()
88             self.latest_version = self.latest_version.replace("\n","")
89             if(re.match('^\d+(\.\d+)*$', self.latest_version)):
90                 self.config = config
91                 self.current_version = ELECTRUM_VERSION
92                 if(self.compare_versions(self.latest_version, self.current_version) == 1):
93                     latest_seen = self.config.get("last_seen_version",ELECTRUM_VERSION)
94                     if(self.compare_versions(self.latest_version, latest_seen) == 1):
95                         self.new_version = True
96                         self.setText(_("New version available") + ": " + self.latest_version)
97
98
99     def compare_versions(self, version1, version2):
100         def normalize(v):
101             return [int(x) for x in re.sub(r'(\.0+)*$','', v).split(".")]
102         return cmp(normalize(version1), normalize(version2))
103
104     def ignore_this_version(self):
105         self.setText("")
106         self.config.set_key("last_seen_version", self.latest_version, True)
107         QMessageBox.information(self, _("Preference saved"), _("Notifications about this update will not be shown again."))
108         self.dialog.done(0)
109
110     def ignore_all_version(self):
111         self.setText("")
112         self.config.set_key("last_seen_version", "9.9.9", True)
113         QMessageBox.information(self, _("Preference saved"), _("No more notifications about version updates will be shown."))
114         self.dialog.done(0)
115   
116     def open_website(self):
117         webbrowser.open("http://electrum.org/download.html")
118         self.dialog.done(0)
119
120     def mouseReleaseEvent(self, event):
121         dialog = QDialog(self)
122         dialog.setWindowTitle(_('Electrum update'))
123         dialog.setModal(1)
124
125         main_layout = QGridLayout()
126         main_layout.addWidget(QLabel(_("A new version of Electrum is available:")+" " + self.latest_version), 0,0,1,3)
127         
128         ignore_version = QPushButton(_("Ignore this version"))
129         ignore_version.clicked.connect(self.ignore_this_version)
130
131         ignore_all_versions = QPushButton(_("Ignore all versions"))
132         ignore_all_versions.clicked.connect(self.ignore_all_version)
133
134         open_website = QPushButton(_("Goto download page"))
135         open_website.clicked.connect(self.open_website)
136
137         main_layout.addWidget(ignore_version, 1, 0)
138         main_layout.addWidget(ignore_all_versions, 1, 1)
139         main_layout.addWidget(open_website, 1, 2)
140
141         dialog.setLayout(main_layout)
142
143         self.dialog = dialog
144         
145         if not dialog.exec_(): return
146
147
148
149 class Timer(QtCore.QThread):
150     def run(self):
151         while True:
152             self.emit(QtCore.SIGNAL('timersignal'))
153             time.sleep(0.5)
154
155 class HelpButton(QPushButton):
156     def __init__(self, text):
157         QPushButton.__init__(self, '?')
158         self.setFocusPolicy(Qt.NoFocus)
159         self.setFixedWidth(20)
160         self.clicked.connect(lambda: QMessageBox.information(self, 'Help', text, 'OK') )
161
162
163 class EnterButton(QPushButton):
164     def __init__(self, text, func):
165         QPushButton.__init__(self, text)
166         self.func = func
167         self.clicked.connect(func)
168
169     def keyPressEvent(self, e):
170         if e.key() == QtCore.Qt.Key_Return:
171             apply(self.func,())
172
173 class MyTreeWidget(QTreeWidget):
174     def __init__(self, parent):
175         QTreeWidget.__init__(self, parent)
176         def ddfr(item):
177             if not item: return
178             for i in range(0,self.viewport().height()/5):
179                 if self.itemAt(QPoint(0,i*5)) == item:
180                     break
181             else:
182                 return
183             for j in range(0,30):
184                 if self.itemAt(QPoint(0,i*5 + j)) != item:
185                     break
186             self.emit(SIGNAL('customContextMenuRequested(const QPoint&)'), QPoint(50, i*5 + j - 1))
187
188         self.connect(self, SIGNAL('itemActivated(QTreeWidgetItem*, int)'), ddfr)
189         
190
191
192
193 class StatusBarButton(QPushButton):
194     def __init__(self, icon, tooltip, func):
195         QPushButton.__init__(self, icon, '')
196         self.setToolTip(tooltip)
197         self.setFlat(True)
198         self.setMaximumWidth(25)
199         self.clicked.connect(func)
200         self.func = func
201
202     def keyPressEvent(self, e):
203         if e.key() == QtCore.Qt.Key_Return:
204             apply(self.func,())
205
206
207
208
209
210 def waiting_dialog(f):
211
212     s = Timer()
213     s.start()
214     w = QDialog()
215     w.resize(200, 70)
216     w.setWindowTitle('Electrum')
217     l = QLabel('')
218     vbox = QVBoxLayout()
219     vbox.addWidget(l)
220     w.setLayout(vbox)
221     w.show()
222     def ff():
223         s = f()
224         if s: l.setText(s)
225         else: w.close()
226     w.connect(s, QtCore.SIGNAL('timersignal'), ff)
227     w.exec_()
228     w.destroy()
229
230
231
232
233
234
235 default_column_widths = { "history":[40,140,350,140], "contacts":[350,330], "receive":[[370], [370,200,130]] }
236
237 class ElectrumWindow(QMainWindow):
238
239     def __init__(self, wallet, config):
240         QMainWindow.__init__(self)
241         self.lite = None
242         self.wallet = wallet
243         self.config = config
244         self.current_account = self.config.get("current_account", None)
245
246         self.init_plugins()
247         self.create_status_bar()
248
249         self.need_update = threading.Event()
250         self.wallet.interface.register_callback('updated', lambda: self.need_update.set())
251         self.wallet.interface.register_callback('banner', lambda: self.emit(QtCore.SIGNAL('banner_signal')))
252         self.wallet.interface.register_callback('disconnected', lambda: self.emit(QtCore.SIGNAL('update_status')))
253         self.wallet.interface.register_callback('disconnecting', lambda: self.emit(QtCore.SIGNAL('update_status')))
254
255         self.expert_mode = config.get('classic_expert_mode', False)
256         self.decimal_point = config.get('decimal_point', 8)
257
258         set_language(config.get('language'))
259
260         self.funds_error = False
261         self.completions = QStringListModel()
262
263         self.tabs = tabs = QTabWidget(self)
264         self.column_widths = self.config.get("column_widths", default_column_widths )
265         tabs.addTab(self.create_history_tab(), _('History') )
266         tabs.addTab(self.create_send_tab(), _('Send') )
267         tabs.addTab(self.create_receive_tab(), _('Receive') )
268         tabs.addTab(self.create_contacts_tab(), _('Contacts') )
269         tabs.addTab(self.create_console_tab(), _('Console') )
270         tabs.setMinimumSize(600, 400)
271         tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
272         self.setCentralWidget(tabs)
273
274         g = self.config.get("winpos-qt",[100, 100, 840, 400])
275         self.setGeometry(g[0], g[1], g[2], g[3])
276         title = 'Electrum ' + self.wallet.electrum_version + '  -  ' + self.config.path
277         if not self.wallet.seed: title += ' [%s]' % (_('seedless'))
278         self.setWindowTitle( title )
279
280         QShortcut(QKeySequence("Ctrl+W"), self, self.close)
281         QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
282         QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
283         QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
284         
285         self.connect(self, QtCore.SIGNAL('update_status'), self.update_status)
286         self.connect(self, QtCore.SIGNAL('banner_signal'), lambda: self.console.showMessage(self.wallet.interface.banner) )
287         self.history_list.setFocus(True)
288         
289         self.exchanger = exchange_rate.Exchanger(self)
290         self.connect(self, SIGNAL("refresh_balance()"), self.update_wallet)
291
292         # dark magic fix by flatfly; https://bitcointalk.org/index.php?topic=73651.msg959913#msg959913
293         if platform.system() == 'Windows':
294             n = 3 if self.wallet.seed else 2
295             tabs.setCurrentIndex (n)
296             tabs.setCurrentIndex (0)
297
298         # set initial message
299         self.console.showMessage(self.wallet.interface.banner)
300
301         # plugins that need to change the GUI do it here
302         self.run_hook('init_gui')
303
304
305     # plugins
306     def init_plugins(self):
307         import imp, pkgutil, __builtin__
308         if __builtin__.use_local_modules:
309             fp, pathname, description = imp.find_module('plugins')
310             plugin_names = [name for a, name, b in pkgutil.iter_modules([pathname])]
311             plugin_names = filter( lambda name: os.path.exists(os.path.join(pathname,name+'.py')), plugin_names)
312             imp.load_module('electrum_plugins', fp, pathname, description)
313             plugins = map(lambda name: imp.load_source('electrum_plugins.'+name, os.path.join(pathname,name+'.py')), plugin_names)
314         else:
315             import electrum_plugins
316             plugin_names = [name for a, name, b in pkgutil.iter_modules(electrum_plugins.__path__)]
317             plugins = [ __import__('electrum_plugins.'+name, fromlist=['electrum_plugins']) for name in plugin_names]
318
319         self.plugins = []
320         for p in plugins:
321             try:
322                 self.plugins.append( p.Plugin(self) )
323             except:
324                 print_msg("Error:cannot initialize plugin",p)
325                 traceback.print_exc(file=sys.stdout)
326
327
328     def run_hook(self, name, *args):
329         for p in self.plugins:
330             if not p.is_enabled():
331                 continue
332             try:
333                 f = eval('p.'+name)
334             except:
335                 continue
336             try:
337                 apply(f, args)
338             except:
339                 print_error("Plugin error")
340                 traceback.print_exc(file=sys.stdout)
341                 
342         return
343
344         
345     def set_label(self, name, text = None):
346         changed = False
347         old_text = self.wallet.labels.get(name)
348         if text:
349             if old_text != text:
350                 self.wallet.labels[name] = text
351                 changed = True
352         else:
353             if old_text:
354                 self.wallet.labels.pop(name)
355                 changed = True
356         self.run_hook('set_label', name, text, changed)
357         return changed
358
359
360     # custom wrappers for getOpenFileName and getSaveFileName, that remember the path selected by the user
361     def getOpenFileName(self, title, filter = None):
362         directory = self.config.get('io_dir', os.path.expanduser('~'))
363         fileName = unicode( QFileDialog.getOpenFileName(self, title, directory, filter) )
364         if fileName and directory != os.path.dirname(fileName):
365             self.config.set_key('io_dir', os.path.dirname(fileName), True)
366         return fileName
367
368     def getSaveFileName(self, title, filename, filter = None):
369         directory = self.config.get('io_dir', os.path.expanduser('~'))
370         path = os.path.join( directory, filename )
371         fileName = unicode( QFileDialog.getSaveFileName(self, title, path, filter) )
372         if fileName and directory != os.path.dirname(fileName):
373             self.config.set_key('io_dir', os.path.dirname(fileName), True)
374         return fileName
375
376
377
378     def close(self):
379         QMainWindow.close(self)
380         self.run_hook('close_main_window')
381
382     def connect_slots(self, sender):
383         self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
384         self.previous_payto_e=''
385
386     def timer_actions(self):
387         if self.need_update.is_set():
388             self.update_wallet()
389             self.need_update.clear()
390         self.run_hook('timer_actions')
391     
392     def format_amount(self, x, is_diff=False):
393         return format_satoshis(x, is_diff, self.wallet.num_zeros, self.decimal_point)
394
395     def read_amount(self, x):
396         if x in['.', '']: return None
397         p = pow(10, self.decimal_point)
398         return int( p * Decimal(x) )
399
400     def base_unit(self):
401         assert self.decimal_point in [5,8]
402         return "BTC" if self.decimal_point == 8 else "mBTC"
403
404     def update_status(self):
405         if self.wallet.interface and self.wallet.interface.is_connected:
406             if not self.wallet.up_to_date:
407                 text = _("Synchronizing...")
408                 icon = QIcon(":icons/status_waiting.png")
409             else:
410                 c, u = self.wallet.get_account_balance(self.current_account)
411                 text =  _( "Balance" ) + ": %s "%( self.format_amount(c) ) + self.base_unit()
412                 if u: text +=  " [%s unconfirmed]"%( self.format_amount(u,True).strip() )
413                 text += self.create_quote_text(Decimal(c+u)/100000000)
414                 icon = QIcon(":icons/status_connected.png")
415         else:
416             text = _("Not connected")
417             icon = QIcon(":icons/status_disconnected.png")
418
419         self.status_text = text
420         self.statusBar().showMessage(text)
421         self.status_button.setIcon( icon )
422
423     def update_wallet(self):
424         self.update_status()
425         if self.wallet.up_to_date or not self.wallet.interface.is_connected:
426             self.update_history_tab()
427             self.update_receive_tab()
428             self.update_contacts_tab()
429             self.update_completions()
430
431
432     def create_quote_text(self, btc_balance):
433         quote_currency = self.config.get("currency", "None")
434         quote_balance = self.exchanger.exchange(btc_balance, quote_currency)
435         if quote_balance is None:
436             quote_text = ""
437         else:
438             quote_text = "  (%.2f %s)" % (quote_balance, quote_currency)
439         return quote_text
440         
441     def create_history_tab(self):
442         self.history_list = l = MyTreeWidget(self)
443         l.setColumnCount(5)
444         for i,width in enumerate(self.column_widths['history']):
445             l.setColumnWidth(i, width)
446         l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
447         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
448         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
449
450         l.setContextMenuPolicy(Qt.CustomContextMenu)
451         l.customContextMenuRequested.connect(self.create_history_menu)
452         return l
453
454
455     def create_history_menu(self, position):
456         self.history_list.selectedIndexes() 
457         item = self.history_list.currentItem()
458         if not item: return
459         tx_hash = str(item.data(0, Qt.UserRole).toString())
460         if not tx_hash: return
461         menu = QMenu()
462         #menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
463         menu.addAction(_("Details"), lambda: self.show_tx_details(self.wallet.transactions.get(tx_hash)))
464         menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
465         menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
466
467
468     def show_tx_details(self, tx):
469         dialog = QDialog(self)
470         dialog.setModal(1)
471         dialog.setWindowTitle(_("Transaction Details"))
472         vbox = QVBoxLayout()
473         dialog.setLayout(vbox)
474         dialog.setMinimumSize(600,300)
475
476         tx_hash = tx.hash()
477         if tx_hash in self.wallet.transactions.keys():
478             is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
479             conf, timestamp = self.wallet.verifier.get_confirmations(tx_hash)
480             if timestamp:
481                 time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
482             else:
483                 time_str = 'pending'
484         else:
485             is_mine = False
486
487         vbox.addWidget(QLabel("Transaction ID:"))
488         e  = QLineEdit(tx_hash)
489         e.setReadOnly(True)
490         vbox.addWidget(e)
491
492         vbox.addWidget(QLabel("Date: %s"%time_str))
493         vbox.addWidget(QLabel("Status: %d confirmations"%conf))
494         if is_mine:
495             if fee is not None: 
496                 vbox.addWidget(QLabel("Amount sent: %s"% self.format_amount(v-fee)))
497                 vbox.addWidget(QLabel("Transaction fee: %s"% self.format_amount(fee)))
498             else:
499                 vbox.addWidget(QLabel("Amount sent: %s"% self.format_amount(v)))
500                 vbox.addWidget(QLabel("Transaction fee: unknown"))
501         else:
502             vbox.addWidget(QLabel("Amount received: %s"% self.format_amount(v)))
503
504         vbox.addWidget( self.generate_transaction_information_widget(tx) )
505
506         ok_button = QPushButton(_("Close"))
507         ok_button.setDefault(True)
508         ok_button.clicked.connect(dialog.accept)
509         
510         hbox = QHBoxLayout()
511         hbox.addStretch(1)
512         hbox.addWidget(ok_button)
513         vbox.addLayout(hbox)
514         dialog.exec_()
515
516     def tx_label_clicked(self, item, column):
517         if column==2 and item.isSelected():
518             self.is_edit=True
519             item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
520             self.history_list.editItem( item, column )
521             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
522             self.is_edit=False
523
524     def tx_label_changed(self, item, column):
525         if self.is_edit: 
526             return
527         self.is_edit=True
528         tx_hash = str(item.data(0, Qt.UserRole).toString())
529         tx = self.wallet.transactions.get(tx_hash)
530         text = unicode( item.text(2) )
531         self.set_label(tx_hash, text) 
532         if text: 
533             item.setForeground(2, QBrush(QColor('black')))
534         else:
535             text = self.wallet.get_default_label(tx_hash)
536             item.setText(2, text)
537             item.setForeground(2, QBrush(QColor('gray')))
538         self.is_edit=False
539
540
541     def edit_label(self, is_recv):
542         l = self.receive_list if is_recv else self.contacts_list
543         item = l.currentItem()
544         item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
545         l.editItem( item, 1 )
546         item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
547
548
549
550     def address_label_clicked(self, item, column, l, column_addr, column_label):
551         if column == column_label and item.isSelected():
552             is_editable = item.data(0, 32).toBool()
553             if not is_editable:
554                 return
555             addr = unicode( item.text(column_addr) )
556             label = unicode( item.text(column_label) )
557             item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
558             l.editItem( item, column )
559             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
560
561
562     def address_label_changed(self, item, column, l, column_addr, column_label):
563         if column == column_label:
564             addr = unicode( item.text(column_addr) )
565             text = unicode( item.text(column_label) )
566             is_editable = item.data(0, 32).toBool()
567             if not is_editable:
568                 return
569
570             changed = self.set_label(addr, text)
571             if changed:
572                 self.update_history_tab()
573                 self.update_completions()
574                 
575             self.current_item_changed(item)
576
577         self.run_hook('item_changed', item, column)
578
579
580     def current_item_changed(self, a):
581         self.run_hook('current_item_changed', a)
582
583
584
585     def update_history_tab(self):
586
587         self.history_list.clear()
588         for item in self.wallet.get_tx_history(self.current_account):
589             tx_hash, conf, is_mine, value, fee, balance, timestamp = item
590             if conf > 0:
591                 try:
592                     time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
593                 except:
594                     time_str = "unknown"
595
596             if conf == -1:
597                 time_str = 'unverified'
598                 icon = QIcon(":icons/unconfirmed.png")
599             elif conf == 0:
600                 time_str = 'pending'
601                 icon = QIcon(":icons/unconfirmed.png")
602             elif conf < 6:
603                 icon = QIcon(":icons/clock%d.png"%conf)
604             else:
605                 icon = QIcon(":icons/confirmed.png")
606
607             if value is not None:
608                 v_str = self.format_amount(value, True)
609             else:
610                 v_str = '--'
611
612             balance_str = self.format_amount(balance)
613             
614             if tx_hash:
615                 label, is_default_label = self.wallet.get_label(tx_hash)
616             else:
617                 label = _('Pruned transaction outputs')
618                 is_default_label = False
619
620             item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
621             item.setFont(2, QFont(MONOSPACE_FONT))
622             item.setFont(3, QFont(MONOSPACE_FONT))
623             item.setFont(4, QFont(MONOSPACE_FONT))
624             if value < 0:
625                 item.setForeground(3, QBrush(QColor("#BC1E1E")))
626             if tx_hash:
627                 item.setData(0, Qt.UserRole, tx_hash)
628                 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
629             if is_default_label:
630                 item.setForeground(2, QBrush(QColor('grey')))
631
632             item.setIcon(0, icon)
633             self.history_list.insertTopLevelItem(0,item)
634             
635
636         self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
637
638
639     def create_send_tab(self):
640         w = QWidget()
641
642         grid = QGridLayout()
643         grid.setSpacing(8)
644         grid.setColumnMinimumWidth(3,300)
645         grid.setColumnStretch(5,1)
646
647
648         self.payto_e = QLineEdit()
649         grid.addWidget(QLabel(_('Pay to')), 1, 0)
650         grid.addWidget(self.payto_e, 1, 1, 1, 3)
651             
652         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)
653
654         completer = QCompleter()
655         completer.setCaseSensitivity(False)
656         self.payto_e.setCompleter(completer)
657         completer.setModel(self.completions)
658
659         self.message_e = QLineEdit()
660         grid.addWidget(QLabel(_('Description')), 2, 0)
661         grid.addWidget(self.message_e, 2, 1, 1, 3)
662         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)
663
664         self.amount_e = AmountEdit(self.base_unit)
665         grid.addWidget(QLabel(_('Amount')), 3, 0)
666         grid.addWidget(self.amount_e, 3, 1, 1, 2)
667         grid.addWidget(HelpButton(
668                 _('Amount to be sent.') + '\n\n' \
669                     + _('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.') \
670                     + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.')), 3, 3)
671         
672         self.fee_e = AmountEdit(self.base_unit)
673         grid.addWidget(QLabel(_('Fee')), 4, 0)
674         grid.addWidget(self.fee_e, 4, 1, 1, 2) 
675         grid.addWidget(HelpButton(
676                 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
677                     + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
678                     + _('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)
679         b = ''
680         if self.wallet.seed: 
681             b = EnterButton(_("Send"), self.do_send)
682         else:
683             b = EnterButton(_("Create unsigned transaction"), self.do_send)
684         grid.addWidget(b, 6, 1)
685
686         b = EnterButton(_("Clear"),self.do_clear)
687         grid.addWidget(b, 6, 2)
688
689         self.payto_sig = QLabel('')
690         grid.addWidget(self.payto_sig, 7, 0, 1, 4)
691
692         QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
693         QShortcut(QKeySequence("Down"), w, w.focusNextChild)
694         w.setLayout(grid) 
695
696         w2 = QWidget()
697         vbox = QVBoxLayout()
698         vbox.addWidget(w)
699         vbox.addStretch(1)
700         w2.setLayout(vbox)
701
702         def entry_changed( is_fee ):
703             self.funds_error = False
704
705             if self.amount_e.is_shortcut:
706                 self.amount_e.is_shortcut = False
707                 c, u = self.wallet.get_account_balance(self.current_account)
708                 inputs, total, fee = self.wallet.choose_tx_inputs( c + u, 0, self.current_account)
709                 fee = self.wallet.estimated_fee(inputs)
710                 amount = c + u - fee
711                 self.amount_e.setText( self.format_amount(amount) )
712                 self.fee_e.setText( self.format_amount( fee ) )
713                 return
714                 
715             amount = self.read_amount(str(self.amount_e.text()))
716             fee = self.read_amount(str(self.fee_e.text()))
717
718             if not is_fee: fee = None
719             if amount is None:
720                 return
721             inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee, self.current_account )
722             if not is_fee:
723                 self.fee_e.setText( self.format_amount( fee ) )
724             if inputs:
725                 palette = QPalette()
726                 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
727                 text = self.status_text
728             else:
729                 palette = QPalette()
730                 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
731                 self.funds_error = True
732                 text = _( "Not enough funds" )
733                 c, u = self.wallet.get_frozen_balance()
734                 if c+u: text += ' (' + self.format_amount(c+u).strip() + self.base_unit() + ' ' +_("are frozen") + ')'
735
736             self.statusBar().showMessage(text)
737             self.amount_e.setPalette(palette)
738             self.fee_e.setPalette(palette)
739
740         self.amount_e.textChanged.connect(lambda: entry_changed(False) )
741         self.fee_e.textChanged.connect(lambda: entry_changed(True) )
742
743         self.run_hook('create_send_tab', grid)
744         return w2
745
746
747     def update_completions(self):
748         l = []
749         for addr,label in self.wallet.labels.items():
750             if addr in self.wallet.addressbook:
751                 l.append( label + '  <' + addr + '>')
752
753         self.run_hook('update_completions', l)
754         self.completions.setStringList(l)
755
756
757     def protected(func):
758         return lambda s, *args: s.do_protect(func, args)
759
760
761     @protected
762     def do_send(self, password):
763
764         label = unicode( self.message_e.text() )
765         r = unicode( self.payto_e.text() )
766         r = r.strip()
767
768         # label or alias, with address in brackets
769         m = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
770         to_address = m.group(2) if m else r
771
772         if not is_valid(to_address):
773             QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
774             return
775
776         try:
777             amount = self.read_amount(unicode( self.amount_e.text()))
778         except:
779             QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
780             return
781         try:
782             fee = self.read_amount(unicode( self.fee_e.text()))
783         except:
784             QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
785             return
786
787         try:
788             tx = self.wallet.mktx( [(to_address, amount)], password, fee, account=self.current_account)
789         except BaseException, e:
790             self.show_message(str(e))
791             return
792
793         if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
794             QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
795             return
796
797         self.run_hook('send_tx', tx)
798
799         if label: 
800             self.set_label(tx.hash(), label)
801
802         if tx.is_complete:
803             h = self.wallet.send_tx(tx)
804             waiting_dialog(lambda: False if self.wallet.tx_event.isSet() else _("Please wait..."))
805             status, msg = self.wallet.receive_tx( h )
806             if status:
807                 QMessageBox.information(self, '', _('Payment sent.')+'\n'+msg, _('OK'))
808                 self.do_clear()
809                 self.update_contacts_tab()
810             else:
811                 QMessageBox.warning(self, _('Error'), msg, _('OK'))
812         else:
813             filename = label + '.txn' if label else 'unsigned_%s.txn' % (time.mktime(time.gmtime()))
814             try:
815                 fileName = self.getSaveFileName(_("Select a transaction filename"), filename, "*.txn")
816                 with open(fileName,'w') as f:
817                     f.write(json.dumps(tx.as_dict(),indent=4) + '\n')
818                 QMessageBox.information(self, _('Unsigned transaction created'), _("Unsigned transaction was saved to file:") + " " +fileName, _('OK'))
819             except:
820                 QMessageBox.warning(self, _('Error'), _('Could not write transaction to file'), _('OK'))
821
822
823
824
825     def set_url(self, url):
826         address, amount, label, message, signature, identity, url = util.parse_url(url)
827         if self.base_unit() == 'mBTC': amount = str( 1000* Decimal(amount))
828
829         if label and self.wallet.labels.get(address) != label:
830             if self.question('Give label "%s" to address %s ?'%(label,address)):
831                 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
832                     self.wallet.addressbook.append(address)
833                 self.set_label(address, label)
834
835         self.run_hook('set_url', url, self.show_message, self.question)
836
837         self.tabs.setCurrentIndex(1)
838         label = self.wallet.labels.get(address)
839         m_addr = label + '  <'+ address +'>' if label else address
840         self.payto_e.setText(m_addr)
841
842         self.message_e.setText(message)
843         self.amount_e.setText(amount)
844         if identity:
845             self.set_frozen(self.payto_e,True)
846             self.set_frozen(self.amount_e,True)
847             self.set_frozen(self.message_e,True)
848             self.payto_sig.setText( '      The bitcoin URI was signed by ' + identity )
849         else:
850             self.payto_sig.setVisible(False)
851
852     def do_clear(self):
853         self.payto_sig.setVisible(False)
854         for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
855             e.setText('')
856             self.set_frozen(e,False)
857         self.update_status()
858
859     def set_frozen(self,entry,frozen):
860         if frozen:
861             entry.setReadOnly(True)
862             entry.setFrame(False)
863             palette = QPalette()
864             palette.setColor(entry.backgroundRole(), QColor('lightgray'))
865             entry.setPalette(palette)
866         else:
867             entry.setReadOnly(False)
868             entry.setFrame(True)
869             palette = QPalette()
870             palette.setColor(entry.backgroundRole(), QColor('white'))
871             entry.setPalette(palette)
872
873
874     def toggle_freeze(self,addr):
875         if not addr: return
876         if addr in self.wallet.frozen_addresses:
877             self.wallet.unfreeze(addr)
878         else:
879             self.wallet.freeze(addr)
880         self.update_receive_tab()
881
882     def toggle_priority(self,addr):
883         if not addr: return
884         if addr in self.wallet.prioritized_addresses:
885             self.wallet.unprioritize(addr)
886         else:
887             self.wallet.prioritize(addr)
888         self.update_receive_tab()
889
890
891     def create_list_tab(self, headers):
892         "generic tab creation method"
893         l = MyTreeWidget(self)
894         l.setColumnCount( len(headers) )
895         l.setHeaderLabels( headers )
896
897         w = QWidget()
898         vbox = QVBoxLayout()
899         w.setLayout(vbox)
900
901         vbox.setMargin(0)
902         vbox.setSpacing(0)
903         vbox.addWidget(l)
904         buttons = QWidget()
905         vbox.addWidget(buttons)
906
907         hbox = QHBoxLayout()
908         hbox.setMargin(0)
909         hbox.setSpacing(0)
910         buttons.setLayout(hbox)
911
912         return l,w,hbox
913
914
915     def create_receive_tab(self):
916         l,w,hbox = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
917         l.setContextMenuPolicy(Qt.CustomContextMenu)
918         l.customContextMenuRequested.connect(self.create_receive_menu)
919         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
920         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
921         self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
922         self.receive_list = l
923         self.receive_buttons_hbox = hbox
924         hbox.addStretch(1)
925         return w
926
927
928     def receive_tab_set_mode(self, i):
929         self.save_column_widths()
930         self.expert_mode = (i == 1)
931         self.config.set_key('classic_expert_mode', self.expert_mode, True)
932         self.update_receive_tab()
933
934
935     def save_column_widths(self):
936         if not self.expert_mode:
937             widths = [ self.receive_list.columnWidth(0) ]
938         else:
939             widths = []
940             for i in range(self.receive_list.columnCount() -1):
941                 widths.append(self.receive_list.columnWidth(i))
942         self.column_widths["receive"][self.expert_mode] = widths
943         
944         self.column_widths["history"] = []
945         for i in range(self.history_list.columnCount() - 1):
946             self.column_widths["history"].append(self.history_list.columnWidth(i))
947
948         self.column_widths["contacts"] = []
949         for i in range(self.contacts_list.columnCount() - 1):
950             self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
951
952         self.config.set_key("column_widths", self.column_widths, True)
953
954
955     def create_contacts_tab(self):
956         l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
957         l.setContextMenuPolicy(Qt.CustomContextMenu)
958         l.customContextMenuRequested.connect(self.create_contact_menu)
959         for i,width in enumerate(self.column_widths['contacts']):
960             l.setColumnWidth(i, width)
961
962         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
963         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
964         self.contacts_list = l
965         self.contacts_buttons_hbox = hbox
966         hbox.addWidget(EnterButton(_("New"), self.new_contact_dialog))
967         hbox.addStretch(1)
968         return w
969
970
971     def delete_imported_key(self, addr):
972         if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
973             self.wallet.delete_imported_key(addr)
974             self.update_receive_tab()
975             self.update_history_tab()
976
977
978     def create_receive_menu(self, position):
979         # fixme: this function apparently has a side effect.
980         # if it is not called the menu pops up several times
981         #self.receive_list.selectedIndexes() 
982
983         item = self.receive_list.itemAt(position)
984         if not item: return
985         addr = unicode(item.text(0))
986         if not is_valid(addr): 
987             item.setExpanded(not item.isExpanded())
988             return 
989         menu = QMenu()
990         menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
991         menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")) )
992         menu.addAction(_("Edit label"), lambda: self.edit_label(True))
993         menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
994         menu.addAction(_("Sign message"), lambda: self.sign_message(addr))
995         if addr in self.wallet.imported_keys:
996             menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
997
998         if self.expert_mode:
999             t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
1000             menu.addAction(t, lambda: self.toggle_freeze(addr))
1001             t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
1002             menu.addAction(t, lambda: self.toggle_priority(addr))
1003             
1004         self.run_hook('receive_menu', menu)
1005         menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1006
1007
1008     def payto(self, addr):
1009         if not addr: return
1010         label = self.wallet.labels.get(addr)
1011         m_addr = label + '  <' + addr + '>' if label else addr
1012         self.tabs.setCurrentIndex(1)
1013         self.payto_e.setText(m_addr)
1014         self.amount_e.setFocus()
1015
1016
1017     def delete_contact(self, x):
1018         if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1019             self.wallet.delete_contact(x)
1020             self.set_label(x, None)
1021             self.update_history_tab()
1022             self.update_contacts_tab()
1023             self.update_completions()
1024
1025
1026     def create_contact_menu(self, position):
1027         item = self.contacts_list.itemAt(position)
1028         if not item: return
1029         addr = unicode(item.text(0))
1030         label = unicode(item.text(1))
1031         is_editable = item.data(0,32).toBool()
1032         payto_addr = item.data(0,33).toString()
1033         menu = QMenu()
1034         menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1035         menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1036         menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1037         if is_editable:
1038             menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1039             menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1040
1041         self.run_hook('create_contact_menu', menu, item)
1042         menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1043
1044
1045     def update_receive_item(self, item):
1046         item.setFont(0, QFont(MONOSPACE_FONT))
1047         address = str(item.data(0,0).toString())
1048         label = self.wallet.labels.get(address,'')
1049         item.setData(1,0,label)
1050         item.setData(0,32, True) # is editable
1051
1052         self.run_hook('update_receive_item', address, item)
1053                 
1054         c, u = self.wallet.get_addr_balance(address)
1055         balance = self.format_amount(c + u)
1056         item.setData(2,0,balance)
1057
1058         if self.expert_mode:
1059             if address in self.wallet.frozen_addresses: 
1060                 item.setBackgroundColor(0, QColor('lightblue'))
1061             elif address in self.wallet.prioritized_addresses: 
1062                 item.setBackgroundColor(0, QColor('lightgreen'))
1063         
1064
1065     def update_receive_tab(self):
1066         l = self.receive_list
1067         
1068         l.clear()
1069         l.setColumnHidden(2, not self.expert_mode)
1070         l.setColumnHidden(3, not self.expert_mode)
1071         for i,width in enumerate(self.column_widths['receive'][self.expert_mode]):
1072             l.setColumnWidth(i, width)
1073
1074         if self.current_account is None:
1075             account_items = self.wallet.accounts.items()
1076         elif self.current_account != -1:
1077             account_items = [(self.current_account, self.wallet.accounts.get(self.current_account))]
1078         else:
1079             account_items = []
1080
1081         for k, account in account_items:
1082             name = account.get('name',str(k))
1083             c,u = self.wallet.get_account_balance(k)
1084             account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1085             l.addTopLevelItem(account_item)
1086             account_item.setExpanded(True)
1087             
1088             for is_change in ([0,1] if self.expert_mode else [0]):
1089                 if self.expert_mode:
1090                     name = "Receiving" if not is_change else "Change"
1091                     seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1092                     account_item.addChild(seq_item)
1093                     if not is_change: seq_item.setExpanded(True)
1094                 else:
1095                     seq_item = account_item
1096                 is_red = False
1097                 gap = 0
1098
1099                 for address in account[is_change]:
1100                     h = self.wallet.history.get(address,[])
1101             
1102                     if h == []:
1103                         gap += 1
1104                         if gap > self.wallet.gap_limit:
1105                             is_red = True
1106                     else:
1107                         gap = 0
1108
1109                     num_tx = '*' if h == ['*'] else "%d"%len(h)
1110                     item = QTreeWidgetItem( [ address, '', '', num_tx] )
1111                     self.update_receive_item(item)
1112                     if is_red:
1113                         item.setBackgroundColor(1, QColor('red'))
1114                     seq_item.addChild(item)
1115
1116
1117         if self.wallet.imported_keys and (self.current_account is None or self.current_account == -1):
1118             c,u = self.wallet.get_imported_balance()
1119             account_item = QTreeWidgetItem( [ _('Imported'), '', self.format_amount(c+u), ''] )
1120             l.addTopLevelItem(account_item)
1121             account_item.setExpanded(True)
1122             for address in self.wallet.imported_keys.keys():
1123                 item = QTreeWidgetItem( [ address, '', '', ''] )
1124                 self.update_receive_item(item)
1125                 account_item.addChild(item)
1126                 
1127
1128         # we use column 1 because column 0 may be hidden
1129         l.setCurrentItem(l.topLevelItem(0),1)
1130
1131
1132     def update_contacts_tab(self):
1133
1134         l = self.contacts_list
1135         l.clear()
1136
1137         for address in self.wallet.addressbook:
1138             label = self.wallet.labels.get(address,'')
1139             n = self.wallet.get_num_tx(address)
1140             item = QTreeWidgetItem( [ address, label, "%d"%n] )
1141             item.setFont(0, QFont(MONOSPACE_FONT))
1142             # 32 = label can be edited (bool)
1143             item.setData(0,32, True)
1144             # 33 = payto string
1145             item.setData(0,33, address)
1146             l.addTopLevelItem(item)
1147
1148         self.run_hook('update_contacts_tab', l)
1149         l.setCurrentItem(l.topLevelItem(0))
1150
1151
1152
1153     def create_console_tab(self):
1154         from qt_console import Console
1155         self.console = console = Console()
1156         self.console.history = self.config.get("console-history",[])
1157         self.console.history_index = len(self.console.history)
1158
1159         console.updateNamespace({'wallet' : self.wallet, 'interface' : self.wallet.interface, 'gui':self})
1160         console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1161
1162         c = commands.Commands(self.wallet, self.wallet.interface, lambda: self.console.set_json(True))
1163         methods = {}
1164         def mkfunc(f, method):
1165             return lambda *args: apply( f, (method, args, self.password_dialog ))
1166         for m in dir(c):
1167             if m[0]=='_' or m=='wallet' or m == 'interface': continue
1168             methods[m] = mkfunc(c._run, m)
1169             
1170         console.updateNamespace(methods)
1171         return console
1172
1173     def change_account(self,s):
1174         if s == _("All accounts"):
1175             self.current_account = None
1176         else:
1177             accounts = self.wallet.get_accounts()
1178             for k, v in accounts.items():
1179                 if v == s:
1180                     self.current_account = k
1181         self.update_history_tab()
1182         self.update_status()
1183         self.update_receive_tab()
1184
1185     def create_status_bar(self):
1186         self.status_text = ""
1187         sb = QStatusBar()
1188         sb.setFixedHeight(35)
1189         qtVersion = qVersion()
1190
1191         update_notification = UpdateLabel(self.config)
1192         if(update_notification.new_version):
1193             sb.addPermanentWidget(update_notification)
1194
1195         accounts = self.wallet.get_accounts()
1196         if len(accounts) > 1:
1197             from_combo = QComboBox()
1198             from_combo.addItems([_("All accounts")] + accounts.values())
1199             from_combo.setCurrentIndex(0)
1200             self.connect(from_combo,SIGNAL("activated(QString)"),self.change_account) 
1201             sb.addPermanentWidget(from_combo)
1202
1203         if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1204             sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1205         if self.wallet.seed:
1206             self.lock_icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1207             self.password_button = StatusBarButton( self.lock_icon, _("Password"), lambda: self.change_password_dialog(self.wallet, self) )
1208             sb.addPermanentWidget( self.password_button )
1209         sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1210         if self.wallet.seed:
1211             sb.addPermanentWidget( StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog ) )
1212         self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog ) 
1213         sb.addPermanentWidget( self.status_button )
1214
1215         self.run_hook('create_status_bar', (sb,))
1216
1217         self.setStatusBar(sb)
1218         
1219     def go_lite(self):
1220         import gui_lite
1221         self.config.set_key('gui', 'lite', True)
1222         self.hide()
1223         if self.lite:
1224             self.lite.mini.show()
1225         else:
1226             self.lite = gui_lite.ElectrumGui(self.wallet, self.config, self)
1227             self.lite.main(None)
1228
1229     def new_contact_dialog(self):
1230         text, ok = QInputDialog.getText(self, _('New Contact'), _('Address') + ':')
1231         address = unicode(text)
1232         if ok:
1233             if is_valid(address):
1234                 self.wallet.add_contact(address)
1235                 self.update_contacts_tab()
1236                 self.update_history_tab()
1237                 self.update_completions()
1238             else:
1239                 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1240
1241     def show_master_public_key(self):
1242         dialog = QDialog(self)
1243         dialog.setModal(1)
1244         dialog.setWindowTitle(_("Master Public Key"))
1245
1246         main_text = QTextEdit()
1247         main_text.setText(self.wallet.get_master_public_key())
1248         main_text.setReadOnly(True)
1249         main_text.setMaximumHeight(170)
1250         qrw = QRCodeWidget(self.wallet.get_master_public_key())
1251
1252         ok_button = QPushButton(_("OK"))
1253         ok_button.setDefault(True)
1254         ok_button.clicked.connect(dialog.accept)
1255
1256         main_layout = QGridLayout()
1257         main_layout.addWidget(QLabel(_('Your Master Public Key is:')), 0, 0, 1, 2)
1258
1259         main_layout.addWidget(main_text, 1, 0)
1260         main_layout.addWidget(qrw, 1, 1 )
1261
1262         vbox = QVBoxLayout()
1263         vbox.addLayout(main_layout)
1264         hbox = QHBoxLayout()
1265         hbox.addStretch(1)
1266         hbox.addWidget(ok_button)
1267         vbox.addLayout(hbox)
1268
1269         dialog.setLayout(vbox)
1270         dialog.exec_()
1271         
1272
1273     @protected
1274     def show_seed_dialog(self, password):
1275         if not self.wallet.seed:
1276             QMessageBox.information(parent, _('Message'), _('No seed'), _('OK'))
1277             return
1278         try:
1279             seed = self.wallet.decode_seed(password)
1280         except:
1281             QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1282             return
1283         self.show_seed(seed, self.wallet.imported_keys, self)
1284
1285
1286     @classmethod
1287     def show_seed(self, seed, imported_keys, parent=None):
1288         dialog = QDialog(parent)
1289         dialog.setModal(1)
1290         dialog.setWindowTitle('Electrum' + ' - ' + _('Seed'))
1291
1292         brainwallet = ' '.join(mnemonic.mn_encode(seed))
1293
1294         label1 = QLabel(_("Your wallet generation seed is")+ ":")
1295
1296         seed_text = QTextEdit(brainwallet)
1297         seed_text.setReadOnly(True)
1298         seed_text.setMaximumHeight(130)
1299         
1300         msg2 =  _("Please write down or memorize these 12 words (order is important).") + " " \
1301               + _("This seed will allow you to recover your wallet in case of computer failure.") + " " \
1302               + _("Your seed is also displayed as QR code, in case you want to transfer it to a mobile phone.") + "<p>" \
1303               + "<b>"+_("WARNING")+":</b> " + _("Never disclose your seed. Never type it on a website.") + "</b><p>"
1304         if imported_keys:
1305             msg2 += "<b>"+_("WARNING")+":</b> " + _("Your wallet contains imported keys. These keys cannot be recovered from seed.") + "</b><p>"
1306         label2 = QLabel(msg2)
1307         label2.setWordWrap(True)
1308
1309         logo = QLabel()
1310         logo.setPixmap(QPixmap(":icons/seed.png").scaledToWidth(56))
1311         logo.setMaximumWidth(60)
1312
1313         qrw = QRCodeWidget(seed)
1314
1315         ok_button = QPushButton(_("OK"))
1316         ok_button.setDefault(True)
1317         ok_button.clicked.connect(dialog.accept)
1318
1319         grid = QGridLayout()
1320         #main_layout.addWidget(logo, 0, 0)
1321
1322         grid.addWidget(logo, 0, 0)
1323         grid.addWidget(label1, 0, 1)
1324
1325         grid.addWidget(seed_text, 1, 0, 1, 2)
1326
1327         grid.addWidget(qrw, 0, 2, 2, 1)
1328
1329         vbox = QVBoxLayout()
1330         vbox.addLayout(grid)
1331         vbox.addWidget(label2)
1332
1333         hbox = QHBoxLayout()
1334         hbox.addStretch(1)
1335         hbox.addWidget(ok_button)
1336         vbox.addLayout(hbox)
1337
1338         dialog.setLayout(vbox)
1339         dialog.exec_()
1340
1341     def show_qrcode(self, data, title = "QR code"):
1342         if not data: return
1343         d = QDialog(self)
1344         d.setModal(1)
1345         d.setWindowTitle(title)
1346         d.setMinimumSize(270, 300)
1347         vbox = QVBoxLayout()
1348         qrw = QRCodeWidget(data)
1349         vbox.addWidget(qrw, 1)
1350         vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1351         hbox = QHBoxLayout()
1352         hbox.addStretch(1)
1353
1354         def print_qr(self):
1355             filename = "qrcode.bmp"
1356             bmp.save_qrcode(qrw.qr, filename)
1357             QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1358
1359         b = QPushButton(_("Save"))
1360         hbox.addWidget(b)
1361         b.clicked.connect(print_qr)
1362
1363         b = QPushButton(_("Close"))
1364         hbox.addWidget(b)
1365         b.clicked.connect(d.accept)
1366         b.setDefault(True)
1367
1368         vbox.addLayout(hbox)
1369         d.setLayout(vbox)
1370         d.exec_()
1371
1372
1373     def do_protect(self, func, args):
1374         if self.wallet.use_encryption:
1375             password = self.password_dialog()
1376             if not password:
1377                 return
1378         else:
1379             password = None
1380             
1381         if args != (False,):
1382             args = (self,) + args + (password,)
1383         else:
1384             args = (self,password)
1385         apply( func, args)
1386
1387
1388     @protected
1389     def show_private_key(self, address, password):
1390         if not address: return
1391         try:
1392             pk = self.wallet.get_private_key(address, password)
1393         except BaseException, e:
1394             self.show_message(str(e))
1395             return
1396         QMessageBox.information(self, _('Private key'), 'Address'+ ': ' + address + '\n\n' + _('Private key') + ': ' + pk, _('OK'))
1397
1398
1399     @protected
1400     def do_sign(self, address, message, signature, password):
1401         try:
1402             sig = self.wallet.sign_message(str(address.text()), str(message.toPlainText()), password)
1403             signature.setText(sig)
1404         except BaseException, e:
1405             self.show_message(str(e))
1406
1407     def sign_message(self, address):
1408         if not address: return
1409         d = QDialog(self)
1410         d.setModal(1)
1411         d.setWindowTitle(_('Sign Message'))
1412         d.setMinimumSize(410, 290)
1413
1414         tab_widget = QTabWidget()
1415         tab = QWidget()
1416         layout = QGridLayout(tab)
1417
1418         sign_address = QLineEdit()
1419
1420         sign_address.setText(address)
1421         layout.addWidget(QLabel(_('Address')), 1, 0)
1422         layout.addWidget(sign_address, 1, 1)
1423
1424         sign_message = QTextEdit()
1425         layout.addWidget(QLabel(_('Message')), 2, 0)
1426         layout.addWidget(sign_message, 2, 1)
1427         layout.setRowStretch(2,3)
1428
1429         sign_signature = QTextEdit()
1430         layout.addWidget(QLabel(_('Signature')), 3, 0)
1431         layout.addWidget(sign_signature, 3, 1)
1432         layout.setRowStretch(3,1)
1433
1434
1435         hbox = QHBoxLayout()
1436         b = QPushButton(_("Sign"))
1437         hbox.addWidget(b)
1438         b.clicked.connect(lambda: self.do_sign(sign_address, sign_message, sign_signature))
1439         b = QPushButton(_("Close"))
1440         b.clicked.connect(d.accept)
1441         hbox.addWidget(b)
1442         layout.addLayout(hbox, 4, 1)
1443         tab_widget.addTab(tab, _("Sign"))
1444
1445
1446         tab = QWidget()
1447         layout = QGridLayout(tab)
1448
1449         verify_address = QLineEdit()
1450         layout.addWidget(QLabel(_('Address')), 1, 0)
1451         layout.addWidget(verify_address, 1, 1)
1452
1453         verify_message = QTextEdit()
1454         layout.addWidget(QLabel(_('Message')), 2, 0)
1455         layout.addWidget(verify_message, 2, 1)
1456         layout.setRowStretch(2,3)
1457
1458         verify_signature = QTextEdit()
1459         layout.addWidget(QLabel(_('Signature')), 3, 0)
1460         layout.addWidget(verify_signature, 3, 1)
1461         layout.setRowStretch(3,1)
1462
1463         def do_verify():
1464             if self.wallet.verify_message(verify_address.text(), str(verify_signature.toPlainText()), str(verify_message.toPlainText())):
1465                 self.show_message(_("Signature verified"))
1466             else:
1467                 self.show_message(_("Error: wrong signature"))
1468
1469         hbox = QHBoxLayout()
1470         b = QPushButton(_("Verify"))
1471         b.clicked.connect(do_verify)
1472         hbox.addWidget(b)
1473         b = QPushButton(_("Close"))
1474         b.clicked.connect(d.accept)
1475         hbox.addWidget(b)
1476         layout.addLayout(hbox, 4, 1)
1477         tab_widget.addTab(tab, _("Verify"))
1478
1479         vbox = QVBoxLayout()
1480         vbox.addWidget(tab_widget)
1481         d.setLayout(vbox)
1482         d.exec_()
1483
1484         
1485
1486
1487     def question(self, msg):
1488         return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1489
1490     def show_message(self, msg):
1491         QMessageBox.information(self, _('Message'), msg, _('OK'))
1492
1493     def password_dialog(self ):
1494         d = QDialog(self)
1495         d.setModal(1)
1496
1497         pw = QLineEdit()
1498         pw.setEchoMode(2)
1499
1500         vbox = QVBoxLayout()
1501         msg = _('Please enter your password')
1502         vbox.addWidget(QLabel(msg))
1503
1504         grid = QGridLayout()
1505         grid.setSpacing(8)
1506         grid.addWidget(QLabel(_('Password')), 1, 0)
1507         grid.addWidget(pw, 1, 1)
1508         vbox.addLayout(grid)
1509
1510         vbox.addLayout(ok_cancel_buttons(d))
1511         d.setLayout(vbox)
1512
1513         self.run_hook('password_dialog', pw, grid, 1)
1514         if not d.exec_(): return
1515         return unicode(pw.text())
1516
1517
1518
1519
1520
1521     @staticmethod
1522     def change_password_dialog( wallet, parent=None ):
1523
1524         if not wallet.seed:
1525             QMessageBox.information(parent, _('Error'), _('No seed'), _('OK'))
1526             return
1527
1528         d = QDialog(parent)
1529         d.setModal(1)
1530
1531         pw = QLineEdit()
1532         pw.setEchoMode(2)
1533         new_pw = QLineEdit()
1534         new_pw.setEchoMode(2)
1535         conf_pw = QLineEdit()
1536         conf_pw.setEchoMode(2)
1537
1538         vbox = QVBoxLayout()
1539         if parent:
1540             msg = (_('Your wallet is encrypted. Use this dialog to change your password.')+'\n'\
1541                    +_('To disable wallet encryption, enter an empty new password.')) \
1542                    if wallet.use_encryption else _('Your wallet keys are not encrypted')
1543         else:
1544             msg = _("Please choose a password to encrypt your wallet keys.")+'\n'\
1545                   +_("Leave these fields empty if you want to disable encryption.")
1546         vbox.addWidget(QLabel(msg))
1547
1548         grid = QGridLayout()
1549         grid.setSpacing(8)
1550
1551         if wallet.use_encryption:
1552             grid.addWidget(QLabel(_('Password')), 1, 0)
1553             grid.addWidget(pw, 1, 1)
1554
1555         grid.addWidget(QLabel(_('New Password')), 2, 0)
1556         grid.addWidget(new_pw, 2, 1)
1557
1558         grid.addWidget(QLabel(_('Confirm Password')), 3, 0)
1559         grid.addWidget(conf_pw, 3, 1)
1560         vbox.addLayout(grid)
1561
1562         vbox.addLayout(ok_cancel_buttons(d))
1563         d.setLayout(vbox) 
1564
1565         if not d.exec_(): return
1566
1567         password = unicode(pw.text()) if wallet.use_encryption else None
1568         new_password = unicode(new_pw.text())
1569         new_password2 = unicode(conf_pw.text())
1570
1571         try:
1572             seed = wallet.decode_seed(password)
1573         except:
1574             QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
1575             return
1576
1577         if new_password != new_password2:
1578             QMessageBox.warning(parent, _('Error'), _('Passwords do not match'), _('OK'))
1579             return ElectrumWindow.change_password_dialog(wallet, parent) # Retry
1580
1581         try:
1582             wallet.update_password(seed, password, new_password)
1583         except:
1584             QMessageBox.warning(parent, _('Error'), _('Failed to update password'), _('OK'))
1585             return
1586
1587         QMessageBox.information(parent, _('Success'), _('Password was updated successfully'), _('OK'))
1588
1589         if parent: 
1590             icon = QIcon(":icons/lock.png") if wallet.use_encryption else QIcon(":icons/unlock.png")
1591             parent.password_button.setIcon( icon )
1592
1593
1594
1595     def generate_transaction_information_widget(self, tx):
1596         tabs = QTabWidget(self)
1597
1598         tab1 = QWidget()
1599         grid_ui = QGridLayout(tab1)
1600         grid_ui.setColumnStretch(0,1)
1601         tabs.addTab(tab1, _('Outputs') )
1602
1603         tree_widget = MyTreeWidget(self)
1604         tree_widget.setColumnCount(2)
1605         tree_widget.setHeaderLabels( [_('Address'), _('Amount')] )
1606         tree_widget.setColumnWidth(0, 300)
1607         tree_widget.setColumnWidth(1, 50)
1608
1609         for address, value in tx.outputs:
1610             item = QTreeWidgetItem( [address, "%s" % ( self.format_amount(value))] )
1611             tree_widget.addTopLevelItem(item)
1612
1613         tree_widget.setMaximumHeight(100)
1614
1615         grid_ui.addWidget(tree_widget)
1616
1617         tab2 = QWidget()
1618         grid_ui = QGridLayout(tab2)
1619         grid_ui.setColumnStretch(0,1)
1620         tabs.addTab(tab2, _('Inputs') )
1621         
1622         tree_widget = MyTreeWidget(self)
1623         tree_widget.setColumnCount(2)
1624         tree_widget.setHeaderLabels( [ _('Address'), _('Previous output')] )
1625
1626         for input_line in tx.inputs:
1627             item = QTreeWidgetItem( [ str(input_line["address"]), str(input_line["prevout_hash"])] )
1628             tree_widget.addTopLevelItem(item)
1629
1630         tree_widget.setMaximumHeight(100)
1631
1632         grid_ui.addWidget(tree_widget)
1633         return tabs
1634
1635
1636     def tx_dict_from_text(self, txt):
1637         try:
1638             tx_dict = json.loads(str(txt))
1639             assert "hex" in tx_dict.keys()
1640             assert "complete" in tx_dict.keys()
1641             if not tx_dict["complete"]:
1642                 assert "input_info" in tx_dict.keys()
1643         except:
1644             QMessageBox.critical(None, "Unable to parse transaction", _("Electrum was unable to parse your transaction"))
1645             return None
1646         return tx_dict
1647
1648
1649     def read_tx_from_file(self):
1650         fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1651         if not fileName:
1652             return
1653         try:
1654             with open(fileName, "r") as f:
1655                 file_content = f.read()
1656         except (ValueError, IOError, os.error), reason:
1657             QMessageBox.critical(None,"Unable to read file or no transaction found", _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1658
1659         return self.tx_dict_from_text(file_content)
1660
1661
1662     @protected
1663     def sign_raw_transaction(self, tx, input_info, dialog ="", password = ""):
1664         try:
1665             self.wallet.signrawtransaction(tx, input_info, [], password)
1666             
1667             fileName = self.getSaveFileName(_("Select where to save your signed transaction"), 'signed_%s.txn' % (tx.hash()[0:8]), "*.txn")
1668             if fileName:
1669                 with open(fileName, "w+") as f:
1670                     f.write(json.dumps(tx.as_dict(),indent=4) + '\n')
1671                 self.show_message(_("Transaction saved successfully"))
1672                 if dialog:
1673                     dialog.done(0)
1674         except BaseException, e:
1675             self.show_message(str(e))
1676     
1677
1678     def send_raw_transaction(self, raw_tx, dialog = ""):
1679         result, result_message = self.wallet.sendtx( raw_tx )
1680         if result:
1681             self.show_message("Transaction successfully sent: %s" % (result_message))
1682             if dialog:
1683                 dialog.done(0)
1684         else:
1685             self.show_message("There was a problem sending your transaction:\n %s" % (result_message))
1686
1687     def do_process_from_text(self):
1688         text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1689         if not text:
1690             return
1691         tx_dict = self.tx_dict_from_text(text)
1692         if tx_dict:
1693             self.create_process_transaction_window(tx_dict)
1694
1695     def do_process_from_file(self):
1696         tx_dict = self.read_tx_from_file()
1697         if tx_dict: 
1698             self.create_process_transaction_window(tx_dict)
1699
1700     def create_process_transaction_window(self, tx_dict):
1701         tx = Transaction(tx_dict["hex"])
1702             
1703         dialog = QDialog(self)
1704         dialog.setMinimumWidth(500)
1705         dialog.setWindowTitle(_('Process raw transaction'))
1706         dialog.setModal(1)
1707
1708         l = QGridLayout()
1709         dialog.setLayout(l)
1710
1711         l.addWidget(QLabel(_("Transaction status:")), 3,0)
1712         l.addWidget(QLabel(_("Actions")), 4,0)
1713
1714         if tx_dict["complete"] == False:
1715             l.addWidget(QLabel(_("Unsigned")), 3,1)
1716             if self.wallet.seed :
1717                 b = QPushButton("Sign transaction")
1718                 input_info = json.loads(tx_dict["input_info"])
1719                 b.clicked.connect(lambda: self.sign_raw_transaction(tx, input_info, dialog))
1720                 l.addWidget(b, 4, 1)
1721             else:
1722                 l.addWidget(QLabel(_("Wallet is de-seeded, can't sign.")), 4,1)
1723         else:
1724             l.addWidget(QLabel(_("Signed")), 3,1)
1725             b = QPushButton("Broadcast transaction")
1726             b.clicked.connect(lambda: self.send_raw_transaction(tx, dialog))
1727             l.addWidget(b,4,1)
1728
1729         l.addWidget( self.generate_transaction_information_widget(tx), 0,0,2,3)
1730         cancelButton = QPushButton(_("Cancel"))
1731         cancelButton.clicked.connect(lambda: dialog.done(0))
1732         l.addWidget(cancelButton, 4,2)
1733
1734         dialog.exec_()
1735
1736
1737     @protected
1738     def do_export_privkeys(self, password):
1739         self.show_message("%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."),  _("Exposing a single private key can compromise your entire wallet!"), _("In particular, DO NOT use 'redeem private key' services proposed by third parties.")))
1740
1741         try:
1742             select_export = _('Select file to export your private keys to')
1743             fileName = self.getSaveFileName(select_export, 'electrum-private-keys.csv', "*.csv")
1744             if fileName:
1745                 with open(fileName, "w+") as csvfile:
1746                     transaction = csv.writer(csvfile)
1747                     transaction.writerow(["address", "private_key"])
1748
1749                     
1750                     for addr, pk in self.wallet.get_private_keys(self.wallet.addresses(True), password).items():
1751                         transaction.writerow(["%34s"%addr,pk])
1752
1753                     self.show_message(_("Private keys exported."))
1754
1755         except (IOError, os.error), reason:
1756             export_error_label = _("Electrum was unable to produce a private key-export.")
1757             QMessageBox.critical(None,"Unable to create csv", export_error_label + "\n" + str(reason))
1758
1759         except BaseException, e:
1760           self.show_message(str(e))
1761           return
1762
1763
1764     def do_import_labels(self):
1765         labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
1766         if not labelsFile: return
1767         try:
1768             f = open(labelsFile, 'r')
1769             data = f.read()
1770             f.close()
1771             for key, value in json.loads(data).items():
1772                 self.wallet.labels[key] = value
1773             self.wallet.save()
1774             QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
1775         except (IOError, os.error), reason:
1776             QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
1777             
1778
1779     def do_export_labels(self):
1780         labels = self.wallet.labels
1781         try:
1782             fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
1783             if fileName:
1784                 with open(fileName, 'w+') as f:
1785                     json.dump(labels, f)
1786                 QMessageBox.information(None, "Labels exported", _("Your labels where exported to")+" '%s'" % str(fileName))
1787         except (IOError, os.error), reason:
1788             QMessageBox.critical(None, "Unable to export labels", _("Electrum was unable to export your labels.")+"\n" + str(reason))
1789
1790
1791     def do_export_history(self):
1792         from gui_lite import csv_transaction
1793         csv_transaction(self.wallet)
1794
1795
1796     @protected
1797     def do_import_privkey(self, password):
1798         if not self.wallet.imported_keys:
1799             r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
1800                                          + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
1801                                          + _('Are you sure you understand what you are doing?'), 3, 4)
1802             if r == 4: return
1803
1804         text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
1805         if not text: return
1806
1807         text = str(text).split()
1808         badkeys = []
1809         addrlist = []
1810         for key in text:
1811             try:
1812                 addr = self.wallet.import_key(key, password)
1813             except BaseException as e:
1814                 badkeys.append(key)
1815                 continue
1816             if not addr: 
1817                 badkeys.append(key)
1818             else:
1819                 addrlist.append(addr)
1820         if addrlist:
1821             QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
1822         if badkeys:
1823             QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
1824         self.update_receive_tab()
1825         self.update_history_tab()
1826
1827
1828     def settings_dialog(self):
1829         d = QDialog(self)
1830         d.setWindowTitle(_('Electrum Settings'))
1831         d.setModal(1)
1832         vbox = QVBoxLayout()
1833
1834         tabs = QTabWidget(self)
1835         self.settings_tab = tabs
1836         vbox.addWidget(tabs)
1837
1838         tab1 = QWidget()
1839         grid_ui = QGridLayout(tab1)
1840         grid_ui.setColumnStretch(0,1)
1841         tabs.addTab(tab1, _('Display') )
1842
1843         nz_label = QLabel(_('Display zeros'))
1844         grid_ui.addWidget(nz_label, 0, 0)
1845         nz_e = AmountEdit(None,True)
1846         nz_e.setText("%d"% self.wallet.num_zeros)
1847         grid_ui.addWidget(nz_e, 0, 1)
1848         msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1849         grid_ui.addWidget(HelpButton(msg), 0, 2)
1850         if not self.config.is_modifiable('num_zeros'):
1851             for w in [nz_e, nz_label]: w.setEnabled(False)
1852         
1853         lang_label=QLabel(_('Language') + ':')
1854         grid_ui.addWidget(lang_label, 1, 0)
1855         lang_combo = QComboBox()
1856         from i18n import languages
1857         lang_combo.addItems(languages.values())
1858         try:
1859             index = languages.keys().index(self.config.get("language",''))
1860         except:
1861             index = 0
1862         lang_combo.setCurrentIndex(index)
1863         grid_ui.addWidget(lang_combo, 1, 1)
1864         grid_ui.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
1865         if not self.config.is_modifiable('language'):
1866             for w in [lang_combo, lang_label]: w.setEnabled(False)
1867
1868         currencies = self.exchanger.get_currencies()
1869         currencies.insert(0, "None")
1870
1871         cur_label=QLabel(_('Currency') + ':')
1872         grid_ui.addWidget(cur_label , 2, 0)
1873         cur_combo = QComboBox()
1874         cur_combo.addItems(currencies)
1875         try:
1876             index = currencies.index(self.config.get('currency', "None"))
1877         except:
1878             index = 0
1879         cur_combo.setCurrentIndex(index)
1880         grid_ui.addWidget(cur_combo, 2, 1)
1881         grid_ui.addWidget(HelpButton(_('Select which currency is used for quotes.')+' '), 2, 2)
1882         
1883         expert_cb = QCheckBox(_('Expert mode'))
1884         expert_cb.setChecked(self.expert_mode)
1885         grid_ui.addWidget(expert_cb, 3, 0)
1886         hh =  _('In expert mode, your client will:') + '\n'  \
1887             + _(' - Show change addresses in the Receive tab') + '\n'  \
1888             + _(' - Display the balance of each address') + '\n'  \
1889             + _(' - Add freeze/prioritize actions to addresses.') 
1890         grid_ui.addWidget(HelpButton(hh), 3, 2)
1891         grid_ui.setRowStretch(4,1)
1892
1893         # wallet tab
1894         tab2 = QWidget()
1895         grid_wallet = QGridLayout(tab2)
1896         grid_wallet.setColumnStretch(0,1)
1897         tabs.addTab(tab2, _('Wallet') )
1898         
1899         fee_label = QLabel(_('Transaction fee'))
1900         grid_wallet.addWidget(fee_label, 0, 0)
1901         fee_e = AmountEdit(self.base_unit)
1902         fee_e.setText(self.format_amount(self.wallet.fee).strip())
1903         grid_wallet.addWidget(fee_e, 0, 2)
1904         msg = _('Fee per kilobyte of transaction.') + ' ' \
1905             + _('Recommended value') + ': ' + self.format_amount(20000)
1906         grid_wallet.addWidget(HelpButton(msg), 0, 3)
1907         if not self.config.is_modifiable('fee_per_kb'):
1908             for w in [fee_e, fee_label]: w.setEnabled(False)
1909
1910         usechange_cb = QCheckBox(_('Use change addresses'))
1911         usechange_cb.setChecked(self.wallet.use_change)
1912         grid_wallet.addWidget(usechange_cb, 1, 0)
1913         grid_wallet.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 1, 3)
1914         if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
1915
1916         gap_label = QLabel(_('Gap limit'))
1917         grid_wallet.addWidget(gap_label, 2, 0)
1918         gap_e = AmountEdit(None,True)
1919         gap_e.setText("%d"% self.wallet.gap_limit)
1920         grid_wallet.addWidget(gap_e, 2, 2)
1921         msg =  _('The gap limit is the maximal number of contiguous unused addresses in your sequence of receiving addresses.') + '\n' \
1922               + _('You may increase it if you need more receiving addresses.') + '\n\n' \
1923               + _('Your current gap limit is') + ': %d'%self.wallet.gap_limit + '\n' \
1924               + _('Given the current status of your address sequence, the minimum gap limit you can use is:')+' ' + '%d'%self.wallet.min_acceptable_gap() + '\n\n' \
1925               + _('Warning') + ': ' \
1926               + _('The gap limit parameter must be provided in order to recover your wallet from seed.') + ' ' \
1927               + _('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' 
1928         grid_wallet.addWidget(HelpButton(msg), 2, 3)
1929         if not self.config.is_modifiable('gap_limit'):
1930             for w in [gap_e, gap_label]: w.setEnabled(False)
1931
1932         units = ['BTC', 'mBTC']
1933         unit_label = QLabel(_('Base unit'))
1934         grid_wallet.addWidget(unit_label, 3, 0)
1935         unit_combo = QComboBox()
1936         unit_combo.addItems(units)
1937         unit_combo.setCurrentIndex(units.index(self.base_unit()))
1938         grid_wallet.addWidget(unit_combo, 3, 2)
1939         grid_wallet.addWidget(HelpButton(_('Base unit of your wallet.')\
1940                                              + '\n1BTC=1000mBTC.\n' \
1941                                              + _(' This settings affects the fields in the Send tab')+' '), 3, 3)
1942         grid_wallet.setRowStretch(4,1)
1943
1944
1945         # import/export tab
1946         tab3 = QWidget()
1947         grid_io = QGridLayout(tab3)
1948         grid_io.setColumnStretch(0,1)
1949         tabs.addTab(tab3, _('Import/Export') )
1950         
1951         grid_io.addWidget(QLabel(_('Labels')), 1, 0)
1952         grid_io.addWidget(EnterButton(_("Export"), self.do_export_labels), 1, 1)
1953         grid_io.addWidget(EnterButton(_("Import"), self.do_import_labels), 1, 2)
1954         grid_io.addWidget(HelpButton(_('Export your labels as json')), 1, 3)
1955
1956         grid_io.addWidget(QLabel(_('History')), 2, 0)
1957         grid_io.addWidget(EnterButton(_("Export"), self.do_export_history), 2, 1)
1958         grid_io.addWidget(HelpButton(_('Export your transaction history as csv')), 2, 3)
1959
1960         grid_io.addWidget(QLabel(_('Private keys')), 3, 0)
1961
1962         grid_io.addWidget(EnterButton(_("Export"), self.do_export_privkeys), 3, 1)
1963         grid_io.addWidget(EnterButton(_("Import"), self.do_import_privkey), 3, 2)
1964         grid_io.addWidget(HelpButton(_('Import private key')), 3, 3)
1965
1966         grid_io.addWidget(QLabel(_('Master Public Key')), 4, 0)
1967         grid_io.addWidget(EnterButton(_("Show"), self.show_master_public_key), 4, 1)
1968         grid_io.addWidget(HelpButton(_('Your Master Public Key can be used to create receiving addresses, but not to sign transactions.') + ' ' \
1969                               + _('If you give it to someone, they will be able to see your transactions, but not to spend your money.') + ' ' \
1970                               + _('If you restore your wallet from it, a watching-only (deseeded) wallet will be created.')), 4, 3)
1971
1972
1973         grid_io.addWidget(QLabel(_("Load transaction")), 5, 0)
1974         grid_io.addWidget(EnterButton(_("From file"), self.do_process_from_file), 5, 1)
1975         grid_io.addWidget(EnterButton(_("From text"), self.do_process_from_text), 5, 2)
1976         grid_io.addWidget(HelpButton(_("This will give you the option to sign or broadcast a transaction based on it's status.")), 5, 3)
1977
1978         grid_io.setRowStretch(6,1)
1979
1980
1981         # plugins
1982         if self.plugins:
1983             tab5 = QScrollArea()
1984             tab5.setEnabled(True)
1985             tab5.setWidgetResizable(True)
1986
1987             grid_plugins = QGridLayout()
1988             grid_plugins.setColumnStretch(0,1)
1989
1990             w = QWidget()
1991             w.setLayout(grid_plugins)
1992             tab5.setWidget(w)
1993             tab5.setMaximumSize(tab3.size())  # optional
1994
1995             w.setMinimumHeight(len(self.plugins)*35)
1996
1997             tabs.addTab(tab5, _('Plugins') )
1998             def mk_toggle(cb, p):
1999                 return lambda: cb.setChecked(p.toggle())
2000             for i, p in enumerate(self.plugins):
2001                 try:
2002                     name, description = p.get_info()
2003                     cb = QCheckBox(name)
2004                     cb.setDisabled(not p.is_available())
2005                     cb.setChecked(p.is_enabled())
2006                     cb.clicked.connect(mk_toggle(cb,p))
2007                     grid_plugins.addWidget(cb, i, 0)
2008                     if p.requires_settings():
2009                         grid_plugins.addWidget(EnterButton(_('Settings'), p.settings_dialog), i, 1)
2010                     grid_plugins.addWidget(HelpButton(description), i, 2)
2011                 except:
2012                     print_msg("Error: cannot display plugin", p)
2013                     traceback.print_exc(file=sys.stdout)
2014             grid_plugins.setRowStretch(i+1,1)
2015
2016         self.run_hook('create_settings_tab', tabs)
2017
2018         vbox.addLayout(ok_cancel_buttons(d))
2019         d.setLayout(vbox) 
2020
2021         # run the dialog
2022         if not d.exec_(): return
2023
2024         fee = unicode(fee_e.text())
2025         try:
2026             fee = self.read_amount(fee)
2027         except:
2028             QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2029             return
2030
2031         self.wallet.set_fee(fee)
2032         
2033         nz = unicode(nz_e.text())
2034         try:
2035             nz = int( nz )
2036             if nz>8: nz=8
2037         except:
2038             QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2039             return
2040
2041         if self.wallet.num_zeros != nz:
2042             self.wallet.num_zeros = nz
2043             self.config.set_key('num_zeros', nz, True)
2044             self.update_history_tab()
2045             self.update_receive_tab()
2046
2047         usechange_result = usechange_cb.isChecked()
2048         if self.wallet.use_change != usechange_result:
2049             self.wallet.use_change = usechange_result
2050             self.config.set_key('use_change', self.wallet.use_change, True)
2051         
2052         unit_result = units[unit_combo.currentIndex()]
2053         if self.base_unit() != unit_result:
2054             self.decimal_point = 8 if unit_result == 'BTC' else 5
2055             self.config.set_key('decimal_point', self.decimal_point, True)
2056             self.update_history_tab()
2057             self.update_status()
2058         
2059         try:
2060             n = int(gap_e.text())
2061         except:
2062             QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
2063             return
2064
2065         if self.wallet.gap_limit != n:
2066             r = self.wallet.change_gap_limit(n)
2067             if r:
2068                 self.update_receive_tab()
2069                 self.config.set_key('gap_limit', self.wallet.gap_limit, True)
2070             else:
2071                 QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
2072
2073         need_restart = False
2074
2075         lang_request = languages.keys()[lang_combo.currentIndex()]
2076         if lang_request != self.config.get('language'):
2077             self.config.set_key("language", lang_request, True)
2078             need_restart = True
2079             
2080         cur_request = str(currencies[cur_combo.currentIndex()])
2081         if cur_request != self.config.get('currency', "None"):
2082             self.config.set_key('currency', cur_request, True)
2083             self.update_wallet()
2084
2085         self.run_hook('close_settings_dialog')
2086
2087         if need_restart:
2088             QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2089
2090         self.receive_tab_set_mode(expert_cb.isChecked())
2091
2092     def run_network_dialog(self):
2093         NetworkDialog(self.wallet.interface, self.config, self).do_exec()
2094
2095     def closeEvent(self, event):
2096         g = self.geometry()
2097         self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
2098         self.save_column_widths()
2099         self.config.set_key("console-history",self.console.history[-50:])
2100         event.accept()
2101
2102
2103
2104
2105
2106 class ElectrumGui:
2107
2108     def __init__(self, wallet, config, app=None):
2109         self.wallet = wallet
2110         self.config = config
2111         if app is None:
2112             self.app = QApplication(sys.argv)
2113
2114
2115     def restore_or_create(self):
2116         msg = _("Wallet file not found.")+"\n"+_("Do you want to create a new wallet, or to restore an existing one?")
2117         r = QMessageBox.question(None, _('Message'), msg, _('Create'), _('Restore'), _('Cancel'), 0, 2)
2118         if r==2: return None
2119         return 'restore' if r==1 else 'create'
2120
2121
2122     def verify_seed(self):
2123         r = self.seed_dialog(False)
2124         if r != self.wallet.seed:
2125             QMessageBox.warning(None, _('Error'), 'incorrect seed', 'OK')
2126             return False
2127         else:
2128             return True
2129         
2130
2131
2132     def seed_dialog(self, is_restore=True):
2133         d = QDialog()
2134         d.setModal(1)
2135
2136         vbox = QVBoxLayout()
2137         if is_restore:
2138             msg = _("Please enter your wallet seed (or your master public key if you want to create a watching-only wallet)." + ' ')
2139         else:
2140             msg = _("Your seed is important! To make sure that you have properly saved your seed, please type it here." + ' ')
2141
2142         msg += _("Your seed can be entered as a sequence of words, or as a hexadecimal string."+ '\n')
2143         
2144         label=QLabel(msg)
2145         label.setWordWrap(True)
2146         vbox.addWidget(label)
2147
2148         seed_e = QTextEdit()
2149         seed_e.setMaximumHeight(100)
2150         vbox.addWidget(seed_e)
2151
2152         if is_restore:
2153             grid = QGridLayout()
2154             grid.setSpacing(8)
2155             gap_e = AmountEdit(None, True)
2156             gap_e.setText("5")
2157             grid.addWidget(QLabel(_('Gap limit')), 2, 0)
2158             grid.addWidget(gap_e, 2, 1)
2159             grid.addWidget(HelpButton(_('Keep the default value unless you modified this parameter in your wallet.')), 2, 3)
2160             vbox.addLayout(grid)
2161
2162         vbox.addLayout(ok_cancel_buttons(d))
2163         d.setLayout(vbox) 
2164
2165         if not d.exec_(): return
2166
2167         try:
2168             seed = str(seed_e.toPlainText())
2169             seed.decode('hex')
2170         except:
2171             try:
2172                 seed = mnemonic.mn_decode( seed.split() )
2173             except:
2174                 QMessageBox.warning(None, _('Error'), _('I cannot decode this'), _('OK'))
2175                 return
2176
2177         if not seed:
2178             QMessageBox.warning(None, _('Error'), _('No seed'), _('OK'))
2179             return
2180
2181         if not is_restore:
2182             return seed
2183         else:
2184             try:
2185                 gap = int(unicode(gap_e.text()))
2186             except:
2187                 QMessageBox.warning(None, _('Error'), 'error', 'OK')
2188                 return
2189             return seed, gap
2190
2191
2192     def network_dialog(self):
2193         return NetworkDialog(self.wallet.interface, self.config, None).do_exec()
2194         
2195
2196     def show_seed(self):
2197         ElectrumWindow.show_seed(self.wallet.seed, self.wallet.imported_keys)
2198
2199     def password_dialog(self):
2200         if self.wallet.seed:
2201             ElectrumWindow.change_password_dialog(self.wallet)
2202
2203
2204     def restore_wallet(self):
2205         wallet = self.wallet
2206         # wait until we are connected, because the user might have selected another server
2207         if not wallet.interface.is_connected:
2208             waiting = lambda: False if wallet.interface.is_connected else "%s \n" % (_("Connecting..."))
2209             waiting_dialog(waiting)
2210
2211         waiting = lambda: False if wallet.is_up_to_date() else "%s\n%s %d\n%s %.1f"\
2212             %(_("Please wait..."),_("Addresses generated:"),len(wallet.addresses(True)),_("Kilobytes received:"), wallet.interface.bytes_received/1024.)
2213
2214         wallet.set_up_to_date(False)
2215         wallet.interface.poke('synchronizer')
2216         waiting_dialog(waiting)
2217         if wallet.is_found():
2218             print_error( "Recovery successful" )
2219         else:
2220             QMessageBox.information(None, _('Error'), _("No transactions found for this seed"), _('OK'))
2221
2222         return True
2223
2224     def main(self,url):
2225         s = Timer()
2226         s.start()
2227         w = ElectrumWindow(self.wallet, self.config)
2228         if url: w.set_url(url)
2229         w.app = self.app
2230         w.connect_slots(s)
2231         w.update_wallet()
2232         w.show()
2233
2234         self.app.exec_()
2235
2236