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