move network dialog in a new class; simplify interface.get_servers
[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.wallet.save()
933         self.update_receive_tab()
934
935
936     def save_column_widths(self):
937         if not self.expert_mode:
938             widths = [ self.receive_list.columnWidth(0) ]
939         else:
940             widths = []
941             for i in range(self.receive_list.columnCount() -1):
942                 widths.append(self.receive_list.columnWidth(i))
943         self.column_widths["receive"][self.expert_mode] = widths
944         
945         self.column_widths["history"] = []
946         for i in range(self.history_list.columnCount() - 1):
947             self.column_widths["history"].append(self.history_list.columnWidth(i))
948
949         self.column_widths["contacts"] = []
950         for i in range(self.contacts_list.columnCount() - 1):
951             self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
952
953         self.config.set_key("column_widths", self.column_widths, True)
954
955
956     def create_contacts_tab(self):
957         l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
958         l.setContextMenuPolicy(Qt.CustomContextMenu)
959         l.customContextMenuRequested.connect(self.create_contact_menu)
960         for i,width in enumerate(self.column_widths['contacts']):
961             l.setColumnWidth(i, width)
962
963         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
964         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
965         self.contacts_list = l
966         self.contacts_buttons_hbox = hbox
967         hbox.addWidget(EnterButton(_("New"), self.new_contact_dialog))
968         hbox.addStretch(1)
969         return w
970
971
972     def delete_imported_key(self, addr):
973         if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
974             self.wallet.imported_keys.pop(addr)
975             self.update_receive_tab()
976             self.update_history_tab()
977             self.wallet.save()
978
979
980     def create_receive_menu(self, position):
981         # fixme: this function apparently has a side effect.
982         # if it is not called the menu pops up several times
983         #self.receive_list.selectedIndexes() 
984
985         item = self.receive_list.itemAt(position)
986         if not item: return
987         addr = unicode(item.text(0))
988         if not is_valid(addr): 
989             item.setExpanded(not item.isExpanded())
990             return 
991         menu = QMenu()
992         menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
993         menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")) )
994         menu.addAction(_("Edit label"), lambda: self.edit_label(True))
995         menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
996         menu.addAction(_("Sign message"), lambda: self.sign_message(addr))
997         if addr in self.wallet.imported_keys:
998             menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
999
1000         if self.expert_mode:
1001             t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
1002             menu.addAction(t, lambda: self.toggle_freeze(addr))
1003             t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
1004             menu.addAction(t, lambda: self.toggle_priority(addr))
1005             
1006         self.run_hook('receive_menu', menu)
1007         menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1008
1009
1010     def payto(self, addr):
1011         if not addr: return
1012         label = self.wallet.labels.get(addr)
1013         m_addr = label + '  <' + addr + '>' if label else addr
1014         self.tabs.setCurrentIndex(1)
1015         self.payto_e.setText(m_addr)
1016         self.amount_e.setFocus()
1017
1018
1019     def delete_contact(self, x):
1020         if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1021             if x in self.wallet.addressbook:
1022                 self.wallet.addressbook.remove(x)
1023                 self.set_label(x, None)
1024                 self.update_history_tab()
1025                 self.update_contacts_tab()
1026                 self.update_completions()
1027
1028
1029     def create_contact_menu(self, position):
1030         item = self.contacts_list.itemAt(position)
1031         if not item: return
1032         addr = unicode(item.text(0))
1033         label = unicode(item.text(1))
1034         is_editable = item.data(0,32).toBool()
1035         payto_addr = item.data(0,33).toString()
1036         menu = QMenu()
1037         menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1038         menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1039         menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1040         if is_editable:
1041             menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1042             menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1043
1044         self.run_hook('create_contact_menu', menu, item)
1045         menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1046
1047
1048     def update_receive_item(self, item):
1049         item.setFont(0, QFont(MONOSPACE_FONT))
1050         address = str(item.data(0,0).toString())
1051         label = self.wallet.labels.get(address,'')
1052         item.setData(1,0,label)
1053         item.setData(0,32, True) # is editable
1054
1055         self.run_hook('update_receive_item', address, item)
1056                 
1057         c, u = self.wallet.get_addr_balance(address)
1058         balance = self.format_amount(c + u)
1059         item.setData(2,0,balance)
1060
1061         if self.expert_mode:
1062             if address in self.wallet.frozen_addresses: 
1063                 item.setBackgroundColor(0, QColor('lightblue'))
1064             elif address in self.wallet.prioritized_addresses: 
1065                 item.setBackgroundColor(0, QColor('lightgreen'))
1066         
1067
1068     def update_receive_tab(self):
1069         l = self.receive_list
1070         
1071         l.clear()
1072         l.setColumnHidden(2, not self.expert_mode)
1073         l.setColumnHidden(3, not self.expert_mode)
1074         for i,width in enumerate(self.column_widths['receive'][self.expert_mode]):
1075             l.setColumnWidth(i, width)
1076
1077         if self.current_account is None:
1078             account_items = self.wallet.accounts.items()
1079         elif self.current_account != -1:
1080             account_items = [(self.current_account, self.wallet.accounts.get(self.current_account))]
1081         else:
1082             account_items = []
1083
1084         for k, account in account_items:
1085             name = account.get('name',str(k))
1086             c,u = self.wallet.get_account_balance(k)
1087             account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1088             l.addTopLevelItem(account_item)
1089             account_item.setExpanded(True)
1090             
1091             for is_change in ([0,1] if self.expert_mode else [0]):
1092                 if self.expert_mode:
1093                     name = "Receiving" if not is_change else "Change"
1094                     seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1095                     account_item.addChild(seq_item)
1096                     if not is_change: seq_item.setExpanded(True)
1097                 else:
1098                     seq_item = account_item
1099                 is_red = False
1100                 gap = 0
1101
1102                 for address in account[is_change]:
1103                     h = self.wallet.history.get(address,[])
1104             
1105                     if h == []:
1106                         gap += 1
1107                         if gap > self.wallet.gap_limit:
1108                             is_red = True
1109                     else:
1110                         gap = 0
1111
1112                     num_tx = '*' if h == ['*'] else "%d"%len(h)
1113                     item = QTreeWidgetItem( [ address, '', '', num_tx] )
1114                     self.update_receive_item(item)
1115                     if is_red:
1116                         item.setBackgroundColor(1, QColor('red'))
1117                     seq_item.addChild(item)
1118
1119
1120         if self.wallet.imported_keys and (self.current_account is None or self.current_account == -1):
1121             c,u = self.wallet.get_imported_balance()
1122             account_item = QTreeWidgetItem( [ _('Imported'), '', self.format_amount(c+u), ''] )
1123             l.addTopLevelItem(account_item)
1124             account_item.setExpanded(True)
1125             for address in self.wallet.imported_keys.keys():
1126                 item = QTreeWidgetItem( [ address, '', '', ''] )
1127                 self.update_receive_item(item)
1128                 account_item.addChild(item)
1129                 
1130
1131         # we use column 1 because column 0 may be hidden
1132         l.setCurrentItem(l.topLevelItem(0),1)
1133
1134
1135     def update_contacts_tab(self):
1136
1137         l = self.contacts_list
1138         l.clear()
1139
1140         for address in self.wallet.addressbook:
1141             label = self.wallet.labels.get(address,'')
1142             n = self.wallet.get_num_tx(address)
1143             item = QTreeWidgetItem( [ address, label, "%d"%n] )
1144             item.setFont(0, QFont(MONOSPACE_FONT))
1145             # 32 = label can be edited (bool)
1146             item.setData(0,32, True)
1147             # 33 = payto string
1148             item.setData(0,33, address)
1149             l.addTopLevelItem(item)
1150
1151         self.run_hook('update_contacts_tab', l)
1152         l.setCurrentItem(l.topLevelItem(0))
1153
1154
1155
1156     def create_console_tab(self):
1157         from qt_console import Console
1158         self.console = console = Console()
1159         self.console.history = self.config.get("console-history",[])
1160         self.console.history_index = len(self.console.history)
1161
1162         console.updateNamespace({'wallet' : self.wallet, 'interface' : self.wallet.interface, 'gui':self})
1163         console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1164
1165         c = commands.Commands(self.wallet, self.wallet.interface, lambda: self.console.set_json(True))
1166         methods = {}
1167         def mkfunc(f, method):
1168             return lambda *args: apply( f, (method, args, self.password_dialog ))
1169         for m in dir(c):
1170             if m[0]=='_' or m=='wallet' or m == 'interface': continue
1171             methods[m] = mkfunc(c._run, m)
1172             
1173         console.updateNamespace(methods)
1174         return console
1175
1176     def change_account(self,s):
1177         if s == _("All accounts"):
1178             self.current_account = None
1179         else:
1180             accounts = self.wallet.get_accounts()
1181             for k, v in accounts.items():
1182                 if v == s:
1183                     self.current_account = k
1184         self.update_history_tab()
1185         self.update_status()
1186         self.update_receive_tab()
1187
1188     def create_status_bar(self):
1189         self.status_text = ""
1190         sb = QStatusBar()
1191         sb.setFixedHeight(35)
1192         qtVersion = qVersion()
1193
1194         update_notification = UpdateLabel(self.config)
1195         if(update_notification.new_version):
1196             sb.addPermanentWidget(update_notification)
1197
1198         accounts = self.wallet.get_accounts()
1199         if len(accounts) > 1:
1200             from_combo = QComboBox()
1201             from_combo.addItems([_("All accounts")] + accounts.values())
1202             from_combo.setCurrentIndex(0)
1203             self.connect(from_combo,SIGNAL("activated(QString)"),self.change_account) 
1204             sb.addPermanentWidget(from_combo)
1205
1206         if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1207             sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1208         if self.wallet.seed:
1209             self.lock_icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1210             self.password_button = StatusBarButton( self.lock_icon, _("Password"), lambda: self.change_password_dialog(self.wallet, self) )
1211             sb.addPermanentWidget( self.password_button )
1212         sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1213         if self.wallet.seed:
1214             sb.addPermanentWidget( StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog ) )
1215         self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog ) 
1216         sb.addPermanentWidget( self.status_button )
1217
1218         self.run_hook('create_status_bar', (sb,))
1219
1220         self.setStatusBar(sb)
1221         
1222     def go_lite(self):
1223         import gui_lite
1224         self.config.set_key('gui', 'lite', True)
1225         self.hide()
1226         if self.lite:
1227             self.lite.mini.show()
1228         else:
1229             self.lite = gui_lite.ElectrumGui(self.wallet, self.config, self)
1230             self.lite.main(None)
1231
1232     def new_contact_dialog(self):
1233         text, ok = QInputDialog.getText(self, _('New Contact'), _('Address') + ':')
1234         address = unicode(text)
1235         if ok:
1236             if is_valid(address):
1237                 self.wallet.addressbook.append(address)
1238                 self.wallet.save()
1239                 self.update_contacts_tab()
1240                 self.update_history_tab()
1241                 self.update_completions()
1242             else:
1243                 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1244
1245     def show_master_public_key(self):
1246         dialog = QDialog(self)
1247         dialog.setModal(1)
1248         dialog.setWindowTitle(_("Master Public Key"))
1249
1250         main_text = QTextEdit()
1251         main_text.setText(self.wallet.get_master_public_key())
1252         main_text.setReadOnly(True)
1253         main_text.setMaximumHeight(170)
1254         qrw = QRCodeWidget(self.wallet.get_master_public_key())
1255
1256         ok_button = QPushButton(_("OK"))
1257         ok_button.setDefault(True)
1258         ok_button.clicked.connect(dialog.accept)
1259
1260         main_layout = QGridLayout()
1261         main_layout.addWidget(QLabel(_('Your Master Public Key is:')), 0, 0, 1, 2)
1262
1263         main_layout.addWidget(main_text, 1, 0)
1264         main_layout.addWidget(qrw, 1, 1 )
1265
1266         vbox = QVBoxLayout()
1267         vbox.addLayout(main_layout)
1268         hbox = QHBoxLayout()
1269         hbox.addStretch(1)
1270         hbox.addWidget(ok_button)
1271         vbox.addLayout(hbox)
1272
1273         dialog.setLayout(vbox)
1274         dialog.exec_()
1275         
1276
1277     @protected
1278     def show_seed_dialog(self, password):
1279         if not self.wallet.seed:
1280             QMessageBox.information(parent, _('Message'), _('No seed'), _('OK'))
1281             return
1282         try:
1283             seed = self.wallet.decode_seed(password)
1284         except:
1285             QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1286             return
1287         self.show_seed(seed, self.wallet.imported_keys, self)
1288
1289
1290     @classmethod
1291     def show_seed(self, seed, imported_keys, parent=None):
1292         dialog = QDialog(parent)
1293         dialog.setModal(1)
1294         dialog.setWindowTitle('Electrum' + ' - ' + _('Seed'))
1295
1296         brainwallet = ' '.join(mnemonic.mn_encode(seed))
1297
1298         label1 = QLabel(_("Your wallet generation seed is")+ ":")
1299
1300         seed_text = QTextEdit(brainwallet)
1301         seed_text.setReadOnly(True)
1302         seed_text.setMaximumHeight(130)
1303         
1304         msg2 =  _("Please write down or memorize these 12 words (order is important).") + " " \
1305               + _("This seed will allow you to recover your wallet in case of computer failure.") + " " \
1306               + _("Your seed is also displayed as QR code, in case you want to transfer it to a mobile phone.") + "<p>" \
1307               + "<b>"+_("WARNING")+":</b> " + _("Never disclose your seed. Never type it on a website.") + "</b><p>"
1308         if imported_keys:
1309             msg2 += "<b>"+_("WARNING")+":</b> " + _("Your wallet contains imported keys. These keys cannot be recovered from seed.") + "</b><p>"
1310         label2 = QLabel(msg2)
1311         label2.setWordWrap(True)
1312
1313         logo = QLabel()
1314         logo.setPixmap(QPixmap(":icons/seed.png").scaledToWidth(56))
1315         logo.setMaximumWidth(60)
1316
1317         qrw = QRCodeWidget(seed)
1318
1319         ok_button = QPushButton(_("OK"))
1320         ok_button.setDefault(True)
1321         ok_button.clicked.connect(dialog.accept)
1322
1323         grid = QGridLayout()
1324         #main_layout.addWidget(logo, 0, 0)
1325
1326         grid.addWidget(logo, 0, 0)
1327         grid.addWidget(label1, 0, 1)
1328
1329         grid.addWidget(seed_text, 1, 0, 1, 2)
1330
1331         grid.addWidget(qrw, 0, 2, 2, 1)
1332
1333         vbox = QVBoxLayout()
1334         vbox.addLayout(grid)
1335         vbox.addWidget(label2)
1336
1337         hbox = QHBoxLayout()
1338         hbox.addStretch(1)
1339         hbox.addWidget(ok_button)
1340         vbox.addLayout(hbox)
1341
1342         dialog.setLayout(vbox)
1343         dialog.exec_()
1344
1345     def show_qrcode(self, data, title = "QR code"):
1346         if not data: return
1347         d = QDialog(self)
1348         d.setModal(1)
1349         d.setWindowTitle(title)
1350         d.setMinimumSize(270, 300)
1351         vbox = QVBoxLayout()
1352         qrw = QRCodeWidget(data)
1353         vbox.addWidget(qrw, 1)
1354         vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1355         hbox = QHBoxLayout()
1356         hbox.addStretch(1)
1357
1358         def print_qr(self):
1359             filename = "qrcode.bmp"
1360             bmp.save_qrcode(qrw.qr, filename)
1361             QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1362
1363         b = QPushButton(_("Save"))
1364         hbox.addWidget(b)
1365         b.clicked.connect(print_qr)
1366
1367         b = QPushButton(_("Close"))
1368         hbox.addWidget(b)
1369         b.clicked.connect(d.accept)
1370         b.setDefault(True)
1371
1372         vbox.addLayout(hbox)
1373         d.setLayout(vbox)
1374         d.exec_()
1375
1376
1377     def do_protect(self, func, args):
1378         if self.wallet.use_encryption:
1379             password = self.password_dialog()
1380             if not password:
1381                 return
1382         else:
1383             password = None
1384             
1385         if args != (False,):
1386             args = (self,) + args + (password,)
1387         else:
1388             args = (self,password)
1389         apply( func, args)
1390
1391
1392     @protected
1393     def show_private_key(self, address, password):
1394         if not address: return
1395         try:
1396             pk = self.wallet.get_private_key(address, password)
1397         except BaseException, e:
1398             self.show_message(str(e))
1399             return
1400         QMessageBox.information(self, _('Private key'), 'Address'+ ': ' + address + '\n\n' + _('Private key') + ': ' + pk, _('OK'))
1401
1402
1403     @protected
1404     def do_sign(self, address, message, signature, password):
1405         try:
1406             sig = self.wallet.sign_message(str(address.text()), str(message.toPlainText()), password)
1407             signature.setText(sig)
1408         except BaseException, e:
1409             self.show_message(str(e))
1410
1411     def sign_message(self, address):
1412         if not address: return
1413         d = QDialog(self)
1414         d.setModal(1)
1415         d.setWindowTitle(_('Sign Message'))
1416         d.setMinimumSize(410, 290)
1417
1418         tab_widget = QTabWidget()
1419         tab = QWidget()
1420         layout = QGridLayout(tab)
1421
1422         sign_address = QLineEdit()
1423
1424         sign_address.setText(address)
1425         layout.addWidget(QLabel(_('Address')), 1, 0)
1426         layout.addWidget(sign_address, 1, 1)
1427
1428         sign_message = QTextEdit()
1429         layout.addWidget(QLabel(_('Message')), 2, 0)
1430         layout.addWidget(sign_message, 2, 1)
1431         layout.setRowStretch(2,3)
1432
1433         sign_signature = QTextEdit()
1434         layout.addWidget(QLabel(_('Signature')), 3, 0)
1435         layout.addWidget(sign_signature, 3, 1)
1436         layout.setRowStretch(3,1)
1437
1438
1439         hbox = QHBoxLayout()
1440         b = QPushButton(_("Sign"))
1441         hbox.addWidget(b)
1442         b.clicked.connect(lambda: self.do_sign(sign_address, sign_message, sign_signature))
1443         b = QPushButton(_("Close"))
1444         b.clicked.connect(d.accept)
1445         hbox.addWidget(b)
1446         layout.addLayout(hbox, 4, 1)
1447         tab_widget.addTab(tab, _("Sign"))
1448
1449
1450         tab = QWidget()
1451         layout = QGridLayout(tab)
1452
1453         verify_address = QLineEdit()
1454         layout.addWidget(QLabel(_('Address')), 1, 0)
1455         layout.addWidget(verify_address, 1, 1)
1456
1457         verify_message = QTextEdit()
1458         layout.addWidget(QLabel(_('Message')), 2, 0)
1459         layout.addWidget(verify_message, 2, 1)
1460         layout.setRowStretch(2,3)
1461
1462         verify_signature = QTextEdit()
1463         layout.addWidget(QLabel(_('Signature')), 3, 0)
1464         layout.addWidget(verify_signature, 3, 1)
1465         layout.setRowStretch(3,1)
1466
1467         def do_verify():
1468             try:
1469                 self.wallet.verify_message(verify_address.text(), str(verify_signature.toPlainText()), str(verify_message.toPlainText()))
1470                 self.show_message(_("Signature verified"))
1471             except BaseException, e:
1472                 self.show_message(str(e))
1473                 return
1474
1475         hbox = QHBoxLayout()
1476         b = QPushButton(_("Verify"))
1477         b.clicked.connect(do_verify)
1478         hbox.addWidget(b)
1479         b = QPushButton(_("Close"))
1480         b.clicked.connect(d.accept)
1481         hbox.addWidget(b)
1482         layout.addLayout(hbox, 4, 1)
1483         tab_widget.addTab(tab, _("Verify"))
1484
1485         vbox = QVBoxLayout()
1486         vbox.addWidget(tab_widget)
1487         d.setLayout(vbox)
1488         d.exec_()
1489
1490         
1491
1492
1493     def question(self, msg):
1494         return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1495
1496     def show_message(self, msg):
1497         QMessageBox.information(self, _('Message'), msg, _('OK'))
1498
1499     def password_dialog(self ):
1500         d = QDialog(self)
1501         d.setModal(1)
1502
1503         pw = QLineEdit()
1504         pw.setEchoMode(2)
1505
1506         vbox = QVBoxLayout()
1507         msg = _('Please enter your password')
1508         vbox.addWidget(QLabel(msg))
1509
1510         grid = QGridLayout()
1511         grid.setSpacing(8)
1512         grid.addWidget(QLabel(_('Password')), 1, 0)
1513         grid.addWidget(pw, 1, 1)
1514         vbox.addLayout(grid)
1515
1516         vbox.addLayout(ok_cancel_buttons(d))
1517         d.setLayout(vbox)
1518
1519         self.run_hook('password_dialog', pw, grid, 1)
1520         if not d.exec_(): return
1521         return unicode(pw.text())
1522
1523
1524
1525
1526
1527     @staticmethod
1528     def change_password_dialog( wallet, parent=None ):
1529
1530         if not wallet.seed:
1531             QMessageBox.information(parent, _('Error'), _('No seed'), _('OK'))
1532             return
1533
1534         d = QDialog(parent)
1535         d.setModal(1)
1536
1537         pw = QLineEdit()
1538         pw.setEchoMode(2)
1539         new_pw = QLineEdit()
1540         new_pw.setEchoMode(2)
1541         conf_pw = QLineEdit()
1542         conf_pw.setEchoMode(2)
1543
1544         vbox = QVBoxLayout()
1545         if parent:
1546             msg = (_('Your wallet is encrypted. Use this dialog to change your password.')+'\n'\
1547                    +_('To disable wallet encryption, enter an empty new password.')) \
1548                    if wallet.use_encryption else _('Your wallet keys are not encrypted')
1549         else:
1550             msg = _("Please choose a password to encrypt your wallet keys.")+'\n'\
1551                   +_("Leave these fields empty if you want to disable encryption.")
1552         vbox.addWidget(QLabel(msg))
1553
1554         grid = QGridLayout()
1555         grid.setSpacing(8)
1556
1557         if wallet.use_encryption:
1558             grid.addWidget(QLabel(_('Password')), 1, 0)
1559             grid.addWidget(pw, 1, 1)
1560
1561         grid.addWidget(QLabel(_('New Password')), 2, 0)
1562         grid.addWidget(new_pw, 2, 1)
1563
1564         grid.addWidget(QLabel(_('Confirm Password')), 3, 0)
1565         grid.addWidget(conf_pw, 3, 1)
1566         vbox.addLayout(grid)
1567
1568         vbox.addLayout(ok_cancel_buttons(d))
1569         d.setLayout(vbox) 
1570
1571         if not d.exec_(): return
1572
1573         password = unicode(pw.text()) if wallet.use_encryption else None
1574         new_password = unicode(new_pw.text())
1575         new_password2 = unicode(conf_pw.text())
1576
1577         try:
1578             seed = wallet.decode_seed(password)
1579         except:
1580             QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
1581             return
1582
1583         if new_password != new_password2:
1584             QMessageBox.warning(parent, _('Error'), _('Passwords do not match'), _('OK'))
1585             return ElectrumWindow.change_password_dialog(wallet, parent) # Retry
1586
1587         wallet.update_password(seed, password, new_password)
1588         if parent: 
1589             icon = QIcon(":icons/lock.png") if wallet.use_encryption else QIcon(":icons/unlock.png")
1590             parent.password_button.setIcon( icon )
1591
1592
1593
1594     def generate_transaction_information_widget(self, tx):
1595         tabs = QTabWidget(self)
1596
1597         tab1 = QWidget()
1598         grid_ui = QGridLayout(tab1)
1599         grid_ui.setColumnStretch(0,1)
1600         tabs.addTab(tab1, _('Outputs') )
1601
1602         tree_widget = MyTreeWidget(self)
1603         tree_widget.setColumnCount(2)
1604         tree_widget.setHeaderLabels( [_('Address'), _('Amount')] )
1605         tree_widget.setColumnWidth(0, 300)
1606         tree_widget.setColumnWidth(1, 50)
1607
1608         for address, value in tx.outputs:
1609             item = QTreeWidgetItem( [address, "%s" % ( self.format_amount(value))] )
1610             tree_widget.addTopLevelItem(item)
1611
1612         tree_widget.setMaximumHeight(100)
1613
1614         grid_ui.addWidget(tree_widget)
1615
1616         tab2 = QWidget()
1617         grid_ui = QGridLayout(tab2)
1618         grid_ui.setColumnStretch(0,1)
1619         tabs.addTab(tab2, _('Inputs') )
1620         
1621         tree_widget = MyTreeWidget(self)
1622         tree_widget.setColumnCount(2)
1623         tree_widget.setHeaderLabels( [ _('Address'), _('Previous output')] )
1624
1625         for input_line in tx.inputs:
1626             item = QTreeWidgetItem( [ str(input_line["address"]), str(input_line["prevout_hash"])] )
1627             tree_widget.addTopLevelItem(item)
1628
1629         tree_widget.setMaximumHeight(100)
1630
1631         grid_ui.addWidget(tree_widget)
1632         return tabs
1633
1634
1635     def tx_dict_from_text(self, txt):
1636         try:
1637             tx_dict = json.loads(str(txt))
1638             assert "hex" in tx_dict.keys()
1639             assert "complete" in tx_dict.keys()
1640             if not tx_dict["complete"]:
1641                 assert "input_info" in tx_dict.keys()
1642         except:
1643             QMessageBox.critical(None, "Unable to parse transaction", _("Electrum was unable to parse your transaction"))
1644             return None
1645         return tx_dict
1646
1647
1648     def read_tx_from_file(self):
1649         fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1650         if not fileName:
1651             return
1652         try:
1653             with open(fileName, "r") as f:
1654                 file_content = f.read()
1655         except (ValueError, IOError, os.error), reason:
1656             QMessageBox.critical(None,"Unable to read file or no transaction found", _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1657
1658         return self.tx_dict_from_text(file_content)
1659
1660
1661     @protected
1662     def sign_raw_transaction(self, tx, input_info, dialog ="", password = ""):
1663         try:
1664             self.wallet.signrawtransaction(tx, input_info, [], password)
1665             
1666             fileName = self.getSaveFileName(_("Select where to save your signed transaction"), 'signed_%s.txn' % (tx.hash()[0:8]), "*.txn")
1667             if fileName:
1668                 with open(fileName, "w+") as f:
1669                     f.write(json.dumps(tx.as_dict(),indent=4) + '\n')
1670                 self.show_message(_("Transaction saved successfully"))
1671                 if dialog:
1672                     dialog.done(0)
1673         except BaseException, e:
1674             self.show_message(str(e))
1675     
1676
1677     def send_raw_transaction(self, raw_tx, dialog = ""):
1678         result, result_message = self.wallet.sendtx( raw_tx )
1679         if result:
1680             self.show_message("Transaction successfully sent: %s" % (result_message))
1681             if dialog:
1682                 dialog.done(0)
1683         else:
1684             self.show_message("There was a problem sending your transaction:\n %s" % (result_message))
1685
1686     def do_process_from_text(self):
1687         text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1688         if not text:
1689             return
1690         tx_dict = self.tx_dict_from_text(text)
1691         if tx_dict:
1692             self.create_process_transaction_window(tx_dict)
1693
1694     def do_process_from_file(self):
1695         tx_dict = self.read_tx_from_file()
1696         if tx_dict: 
1697             self.create_process_transaction_window(tx_dict)
1698
1699     def create_process_transaction_window(self, tx_dict):
1700         tx = Transaction(tx_dict["hex"])
1701             
1702         dialog = QDialog(self)
1703         dialog.setMinimumWidth(500)
1704         dialog.setWindowTitle(_('Process raw transaction'))
1705         dialog.setModal(1)
1706
1707         l = QGridLayout()
1708         dialog.setLayout(l)
1709
1710         l.addWidget(QLabel(_("Transaction status:")), 3,0)
1711         l.addWidget(QLabel(_("Actions")), 4,0)
1712
1713         if tx_dict["complete"] == False:
1714             l.addWidget(QLabel(_("Unsigned")), 3,1)
1715             if self.wallet.seed :
1716                 b = QPushButton("Sign transaction")
1717                 input_info = json.loads(tx_dict["input_info"])
1718                 b.clicked.connect(lambda: self.sign_raw_transaction(tx, input_info, dialog))
1719                 l.addWidget(b, 4, 1)
1720             else:
1721                 l.addWidget(QLabel(_("Wallet is de-seeded, can't sign.")), 4,1)
1722         else:
1723             l.addWidget(QLabel(_("Signed")), 3,1)
1724             b = QPushButton("Broadcast transaction")
1725             b.clicked.connect(lambda: self.send_raw_transaction(tx, dialog))
1726             l.addWidget(b,4,1)
1727
1728         l.addWidget( self.generate_transaction_information_widget(tx), 0,0,2,3)
1729         cancelButton = QPushButton(_("Cancel"))
1730         cancelButton.clicked.connect(lambda: dialog.done(0))
1731         l.addWidget(cancelButton, 4,2)
1732
1733         dialog.exec_()
1734
1735
1736     @protected
1737     def do_export_privkeys(self, password):
1738         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.")))
1739
1740         try:
1741             select_export = _('Select file to export your private keys to')
1742             fileName = self.getSaveFileName(select_export, 'electrum-private-keys.csv', "*.csv")
1743             if fileName:
1744                 with open(fileName, "w+") as csvfile:
1745                     transaction = csv.writer(csvfile)
1746                     transaction.writerow(["address", "private_key"])
1747
1748                     
1749                     for addr, pk in self.wallet.get_private_keys(self.wallet.addresses(True), password).items():
1750                         transaction.writerow(["%34s"%addr,pk])
1751
1752                     self.show_message(_("Private keys exported."))
1753
1754         except (IOError, os.error), reason:
1755             export_error_label = _("Electrum was unable to produce a private key-export.")
1756             QMessageBox.critical(None,"Unable to create csv", export_error_label + "\n" + str(reason))
1757
1758         except BaseException, e:
1759           self.show_message(str(e))
1760           return
1761
1762
1763     def do_import_labels(self):
1764         labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
1765         if not labelsFile: return
1766         try:
1767             f = open(labelsFile, 'r')
1768             data = f.read()
1769             f.close()
1770             for key, value in json.loads(data).items():
1771                 self.wallet.labels[key] = value
1772             self.wallet.save()
1773             QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
1774         except (IOError, os.error), reason:
1775             QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
1776             
1777
1778     def do_export_labels(self):
1779         labels = self.wallet.labels
1780         try:
1781             fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
1782             if fileName:
1783                 with open(fileName, 'w+') as f:
1784                     json.dump(labels, f)
1785                 QMessageBox.information(None, "Labels exported", _("Your labels where exported to")+" '%s'" % str(fileName))
1786         except (IOError, os.error), reason:
1787             QMessageBox.critical(None, "Unable to export labels", _("Electrum was unable to export your labels.")+"\n" + str(reason))
1788
1789
1790     def do_export_history(self):
1791         from gui_lite import csv_transaction
1792         csv_transaction(self.wallet)
1793
1794
1795     @protected
1796     def do_import_privkey(self, password):
1797         if not self.wallet.imported_keys:
1798             r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
1799                                          + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
1800                                          + _('Are you sure you understand what you are doing?'), 3, 4)
1801             if r == 4: return
1802
1803         text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
1804         if not text: return
1805
1806         text = str(text).split()
1807         badkeys = []
1808         addrlist = []
1809         for key in text:
1810             try:
1811                 addr = self.wallet.import_key(key, password)
1812             except BaseException as e:
1813                 badkeys.append(key)
1814                 continue
1815             if not addr: 
1816                 badkeys.append(key)
1817             else:
1818                 addrlist.append(addr)
1819         if addrlist:
1820             QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
1821         if badkeys:
1822             QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
1823         self.update_receive_tab()
1824         self.update_history_tab()
1825
1826
1827     def settings_dialog(self):
1828         d = QDialog(self)
1829         d.setWindowTitle(_('Electrum Settings'))
1830         d.setModal(1)
1831         vbox = QVBoxLayout()
1832
1833         tabs = QTabWidget(self)
1834         self.settings_tab = tabs
1835         vbox.addWidget(tabs)
1836
1837         tab1 = QWidget()
1838         grid_ui = QGridLayout(tab1)
1839         grid_ui.setColumnStretch(0,1)
1840         tabs.addTab(tab1, _('Display') )
1841
1842         nz_label = QLabel(_('Display zeros'))
1843         grid_ui.addWidget(nz_label, 0, 0)
1844         nz_e = AmountEdit(None,True)
1845         nz_e.setText("%d"% self.wallet.num_zeros)
1846         grid_ui.addWidget(nz_e, 0, 1)
1847         msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1848         grid_ui.addWidget(HelpButton(msg), 0, 2)
1849         if not self.config.is_modifiable('num_zeros'):
1850             for w in [nz_e, nz_label]: w.setEnabled(False)
1851         
1852         lang_label=QLabel(_('Language') + ':')
1853         grid_ui.addWidget(lang_label, 1, 0)
1854         lang_combo = QComboBox()
1855         from i18n import languages
1856         lang_combo.addItems(languages.values())
1857         try:
1858             index = languages.keys().index(self.config.get("language",''))
1859         except:
1860             index = 0
1861         lang_combo.setCurrentIndex(index)
1862         grid_ui.addWidget(lang_combo, 1, 1)
1863         grid_ui.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
1864         if not self.config.is_modifiable('language'):
1865             for w in [lang_combo, lang_label]: w.setEnabled(False)
1866
1867         currencies = self.exchanger.get_currencies()
1868         currencies.insert(0, "None")
1869
1870         cur_label=QLabel(_('Currency') + ':')
1871         grid_ui.addWidget(cur_label , 2, 0)
1872         cur_combo = QComboBox()
1873         cur_combo.addItems(currencies)
1874         try:
1875             index = currencies.index(self.config.get('currency', "None"))
1876         except:
1877             index = 0
1878         cur_combo.setCurrentIndex(index)
1879         grid_ui.addWidget(cur_combo, 2, 1)
1880         grid_ui.addWidget(HelpButton(_('Select which currency is used for quotes.')+' '), 2, 2)
1881         
1882         expert_cb = QCheckBox(_('Expert mode'))
1883         expert_cb.setChecked(self.expert_mode)
1884         grid_ui.addWidget(expert_cb, 3, 0)
1885         hh =  _('In expert mode, your client will:') + '\n'  \
1886             + _(' - Show change addresses in the Receive tab') + '\n'  \
1887             + _(' - Display the balance of each address') + '\n'  \
1888             + _(' - Add freeze/prioritize actions to addresses.') 
1889         grid_ui.addWidget(HelpButton(hh), 3, 2)
1890         grid_ui.setRowStretch(4,1)
1891
1892         # wallet tab
1893         tab2 = QWidget()
1894         grid_wallet = QGridLayout(tab2)
1895         grid_wallet.setColumnStretch(0,1)
1896         tabs.addTab(tab2, _('Wallet') )
1897         
1898         fee_label = QLabel(_('Transaction fee'))
1899         grid_wallet.addWidget(fee_label, 0, 0)
1900         fee_e = AmountEdit(self.base_unit)
1901         fee_e.setText(self.format_amount(self.wallet.fee).strip())
1902         grid_wallet.addWidget(fee_e, 0, 2)
1903         msg = _('Fee per kilobyte of transaction.') + ' ' \
1904             + _('Recommended value') + ': ' + self.format_amount(20000)
1905         grid_wallet.addWidget(HelpButton(msg), 0, 3)
1906         if not self.config.is_modifiable('fee_per_kb'):
1907             for w in [fee_e, fee_label]: w.setEnabled(False)
1908
1909         usechange_cb = QCheckBox(_('Use change addresses'))
1910         usechange_cb.setChecked(self.wallet.use_change)
1911         grid_wallet.addWidget(usechange_cb, 1, 0)
1912         grid_wallet.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 1, 3)
1913         if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
1914
1915         gap_label = QLabel(_('Gap limit'))
1916         grid_wallet.addWidget(gap_label, 2, 0)
1917         gap_e = AmountEdit(None,True)
1918         gap_e.setText("%d"% self.wallet.gap_limit)
1919         grid_wallet.addWidget(gap_e, 2, 2)
1920         msg =  _('The gap limit is the maximal number of contiguous unused addresses in your sequence of receiving addresses.') + '\n' \
1921               + _('You may increase it if you need more receiving addresses.') + '\n\n' \
1922               + _('Your current gap limit is') + ': %d'%self.wallet.gap_limit + '\n' \
1923               + _('Given the current status of your address sequence, the minimum gap limit you can use is:')+' ' + '%d'%self.wallet.min_acceptable_gap() + '\n\n' \
1924               + _('Warning') + ': ' \
1925               + _('The gap limit parameter must be provided in order to recover your wallet from seed.') + ' ' \
1926               + _('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' 
1927         grid_wallet.addWidget(HelpButton(msg), 2, 3)
1928         if not self.config.is_modifiable('gap_limit'):
1929             for w in [gap_e, gap_label]: w.setEnabled(False)
1930
1931         units = ['BTC', 'mBTC']
1932         unit_label = QLabel(_('Base unit'))
1933         grid_wallet.addWidget(unit_label, 3, 0)
1934         unit_combo = QComboBox()
1935         unit_combo.addItems(units)
1936         unit_combo.setCurrentIndex(units.index(self.base_unit()))
1937         grid_wallet.addWidget(unit_combo, 3, 2)
1938         grid_wallet.addWidget(HelpButton(_('Base unit of your wallet.')\
1939                                              + '\n1BTC=1000mBTC.\n' \
1940                                              + _(' This settings affects the fields in the Send tab')+' '), 3, 3)
1941         grid_wallet.setRowStretch(4,1)
1942
1943
1944         # import/export tab
1945         tab3 = QWidget()
1946         grid_io = QGridLayout(tab3)
1947         grid_io.setColumnStretch(0,1)
1948         tabs.addTab(tab3, _('Import/Export') )
1949         
1950         grid_io.addWidget(QLabel(_('Labels')), 1, 0)
1951         grid_io.addWidget(EnterButton(_("Export"), self.do_export_labels), 1, 1)
1952         grid_io.addWidget(EnterButton(_("Import"), self.do_import_labels), 1, 2)
1953         grid_io.addWidget(HelpButton(_('Export your labels as json')), 1, 3)
1954
1955         grid_io.addWidget(QLabel(_('History')), 2, 0)
1956         grid_io.addWidget(EnterButton(_("Export"), self.do_export_history), 2, 1)
1957         grid_io.addWidget(HelpButton(_('Export your transaction history as csv')), 2, 3)
1958
1959         grid_io.addWidget(QLabel(_('Private keys')), 3, 0)
1960
1961         grid_io.addWidget(EnterButton(_("Export"), self.do_export_privkeys), 3, 1)
1962         grid_io.addWidget(EnterButton(_("Import"), self.do_import_privkey), 3, 2)
1963         grid_io.addWidget(HelpButton(_('Import private key')), 3, 3)
1964
1965         grid_io.addWidget(QLabel(_('Master Public Key')), 4, 0)
1966         grid_io.addWidget(EnterButton(_("Show"), self.show_master_public_key), 4, 1)
1967         grid_io.addWidget(HelpButton(_('Your Master Public Key can be used to create receiving addresses, but not to sign transactions.') + ' ' \
1968                               + _('If you give it to someone, they will be able to see your transactions, but not to spend your money.') + ' ' \
1969                               + _('If you restore your wallet from it, a watching-only (deseeded) wallet will be created.')), 4, 3)
1970
1971
1972         grid_io.addWidget(QLabel(_("Load transaction")), 5, 0)
1973         grid_io.addWidget(EnterButton(_("From file"), self.do_process_from_file), 5, 1)
1974         grid_io.addWidget(EnterButton(_("From text"), self.do_process_from_text), 5, 2)
1975         grid_io.addWidget(HelpButton(_("This will give you the option to sign or broadcast a transaction based on it's status.")), 5, 3)
1976
1977         grid_io.setRowStretch(6,1)
1978
1979
1980         # plugins
1981         if self.plugins:
1982             tab5 = QScrollArea()
1983             tab5.setEnabled(True)
1984             tab5.setWidgetResizable(True)
1985
1986             grid_plugins = QGridLayout()
1987             grid_plugins.setColumnStretch(0,1)
1988
1989             w = QWidget()
1990             w.setLayout(grid_plugins)
1991             tab5.setWidget(w)
1992             tab5.setMaximumSize(tab3.size())  # optional
1993
1994             w.setMinimumHeight(len(self.plugins)*35)
1995
1996             tabs.addTab(tab5, _('Plugins') )
1997             def mk_toggle(cb, p):
1998                 return lambda: cb.setChecked(p.toggle())
1999             for i, p in enumerate(self.plugins):
2000                 try:
2001                     name, description = p.get_info()
2002                     cb = QCheckBox(name)
2003                     cb.setDisabled(not p.is_available())
2004                     cb.setChecked(p.is_enabled())
2005                     cb.clicked.connect(mk_toggle(cb,p))
2006                     grid_plugins.addWidget(cb, i, 0)
2007                     if p.requires_settings():
2008                         grid_plugins.addWidget(EnterButton(_('Settings'), p.settings_dialog), i, 1)
2009                     grid_plugins.addWidget(HelpButton(description), i, 2)
2010                 except:
2011                     print_msg("Error: cannot display plugin", p)
2012                     traceback.print_exc(file=sys.stdout)
2013             grid_plugins.setRowStretch(i+1,1)
2014
2015         self.run_hook('create_settings_tab', tabs)
2016
2017         vbox.addLayout(ok_cancel_buttons(d))
2018         d.setLayout(vbox) 
2019
2020         # run the dialog
2021         if not d.exec_(): return
2022
2023         fee = unicode(fee_e.text())
2024         try:
2025             fee = self.read_amount(fee)
2026         except:
2027             QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2028             return
2029
2030         if self.wallet.fee != fee:
2031             self.wallet.fee = fee
2032             self.wallet.save()
2033         
2034         nz = unicode(nz_e.text())
2035         try:
2036             nz = int( nz )
2037             if nz>8: nz=8
2038         except:
2039             QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2040             return
2041
2042         if self.wallet.num_zeros != nz:
2043             self.wallet.num_zeros = nz
2044             self.config.set_key('num_zeros', nz, True)
2045             self.update_history_tab()
2046             self.update_receive_tab()
2047
2048         usechange_result = usechange_cb.isChecked()
2049         if self.wallet.use_change != usechange_result:
2050             self.wallet.use_change = usechange_result
2051             self.config.set_key('use_change', self.wallet.use_change, True)
2052         
2053         unit_result = units[unit_combo.currentIndex()]
2054         if self.base_unit() != unit_result:
2055             self.decimal_point = 8 if unit_result == 'BTC' else 5
2056             self.config.set_key('decimal_point', self.decimal_point, True)
2057             self.update_history_tab()
2058             self.update_status()
2059         
2060         try:
2061             n = int(gap_e.text())
2062         except:
2063             QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
2064             return
2065
2066         if self.wallet.gap_limit != n:
2067             r = self.wallet.change_gap_limit(n)
2068             if r:
2069                 self.update_receive_tab()
2070                 self.config.set_key('gap_limit', self.wallet.gap_limit, True)
2071             else:
2072                 QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
2073
2074         need_restart = False
2075
2076         lang_request = languages.keys()[lang_combo.currentIndex()]
2077         if lang_request != self.config.get('language'):
2078             self.config.set_key("language", lang_request, True)
2079             need_restart = True
2080             
2081         cur_request = str(currencies[cur_combo.currentIndex()])
2082         if cur_request != self.config.get('currency', "None"):
2083             self.config.set_key('currency', cur_request, True)
2084             self.update_wallet()
2085
2086         self.run_hook('close_settings_dialog')
2087
2088         if need_restart:
2089             QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2090
2091         self.receive_tab_set_mode(expert_cb.isChecked())
2092
2093     def run_network_dialog(self):
2094         NetworkDialog(self.wallet.interface, self.config, self).do_exec()
2095
2096     def closeEvent(self, event):
2097         g = self.geometry()
2098         self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
2099         self.save_column_widths()
2100         self.config.set_key("console-history",self.console.history[-50:])
2101         event.accept()
2102
2103
2104
2105
2106
2107 class ElectrumGui:
2108
2109     def __init__(self, wallet, config, app=None):
2110         self.wallet = wallet
2111         self.config = config
2112         if app is None:
2113             self.app = QApplication(sys.argv)
2114
2115
2116     def restore_or_create(self):
2117         msg = _("Wallet file not found.")+"\n"+_("Do you want to create a new wallet, or to restore an existing one?")
2118         r = QMessageBox.question(None, _('Message'), msg, _('Create'), _('Restore'), _('Cancel'), 0, 2)
2119         if r==2: return None
2120         return 'restore' if r==1 else 'create'
2121
2122
2123     def verify_seed(self):
2124         r = self.seed_dialog(False)
2125         if r != self.wallet.seed:
2126             QMessageBox.warning(None, _('Error'), 'incorrect seed', 'OK')
2127             return False
2128         else:
2129             return True
2130         
2131
2132
2133     def seed_dialog(self, is_restore=True):
2134         d = QDialog()
2135         d.setModal(1)
2136
2137         vbox = QVBoxLayout()
2138         if is_restore:
2139             msg = _("Please enter your wallet seed (or your master public key if you want to create a watching-only wallet)." + ' ')
2140         else:
2141             msg = _("Your seed is important! To make sure that you have properly saved your seed, please type it here." + ' ')
2142
2143         msg += _("Your seed can be entered as a sequence of words, or as a hexadecimal string."+ '\n')
2144         
2145         label=QLabel(msg)
2146         label.setWordWrap(True)
2147         vbox.addWidget(label)
2148
2149         seed_e = QTextEdit()
2150         seed_e.setMaximumHeight(100)
2151         vbox.addWidget(seed_e)
2152
2153         if is_restore:
2154             grid = QGridLayout()
2155             grid.setSpacing(8)
2156             gap_e = AmountEdit(None, True)
2157             gap_e.setText("5")
2158             grid.addWidget(QLabel(_('Gap limit')), 2, 0)
2159             grid.addWidget(gap_e, 2, 1)
2160             grid.addWidget(HelpButton(_('Keep the default value unless you modified this parameter in your wallet.')), 2, 3)
2161             vbox.addLayout(grid)
2162
2163         vbox.addLayout(ok_cancel_buttons(d))
2164         d.setLayout(vbox) 
2165
2166         if not d.exec_(): return
2167
2168         try:
2169             seed = str(seed_e.toPlainText())
2170             seed.decode('hex')
2171         except:
2172             try:
2173                 seed = mnemonic.mn_decode( seed.split() )
2174             except:
2175                 QMessageBox.warning(None, _('Error'), _('I cannot decode this'), _('OK'))
2176                 return
2177
2178         if not seed:
2179             QMessageBox.warning(None, _('Error'), _('No seed'), _('OK'))
2180             return
2181
2182         if not is_restore:
2183             return seed
2184         else:
2185             try:
2186                 gap = int(unicode(gap_e.text()))
2187             except:
2188                 QMessageBox.warning(None, _('Error'), 'error', 'OK')
2189                 return
2190             return seed, gap
2191
2192
2193     def network_dialog(self):
2194         return NetworkDialog(self.wallet.interface, self.config, None).do_exec()
2195         
2196
2197     def show_seed(self):
2198         ElectrumWindow.show_seed(self.wallet.seed, self.wallet.imported_keys)
2199
2200     def password_dialog(self):
2201         if self.wallet.seed:
2202             ElectrumWindow.change_password_dialog(self.wallet)
2203
2204
2205     def restore_wallet(self):
2206         wallet = self.wallet
2207         # wait until we are connected, because the user might have selected another server
2208         if not wallet.interface.is_connected:
2209             waiting = lambda: False if wallet.interface.is_connected else "%s \n" % (_("Connecting..."))
2210             waiting_dialog(waiting)
2211
2212         waiting = lambda: False if wallet.is_up_to_date() else "%s\n%s %d\n%s %.1f"\
2213             %(_("Please wait..."),_("Addresses generated:"),len(wallet.addresses(True)),_("Kilobytes received:"), wallet.interface.bytes_received/1024.)
2214
2215         wallet.set_up_to_date(False)
2216         wallet.interface.poke('synchronizer')
2217         waiting_dialog(waiting)
2218         if wallet.is_found():
2219             print_error( "Recovery successful" )
2220         else:
2221             QMessageBox.information(None, _('Error'), _("No transactions found for this seed"), _('OK'))
2222
2223         return True
2224
2225     def main(self,url):
2226         s = Timer()
2227         s.start()
2228         w = ElectrumWindow(self.wallet, self.config)
2229         if url: w.set_url(url)
2230         w.app = self.app
2231         w.connect_slots(s)
2232         w.update_wallet()
2233         w.show()
2234
2235         self.app.exec_()
2236
2237