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