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