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