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