correct right-click menu during multiple-select
[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         self.pay_from = []
676         self.tabs.emit(SIGNAL('currentChanged(int)'), 1)
677
678
679
680     def update_history_tab(self):
681
682         self.history_list.clear()
683         for item in self.wallet.get_tx_history(self.current_account):
684             tx_hash, conf, is_mine, value, fee, balance, timestamp = item
685             time_str = _("unknown")
686             if conf > 0:
687                 try:
688                     time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
689                 except Exception:
690                     time_str = _("error")
691
692             if conf == -1:
693                 time_str = 'unverified'
694                 icon = QIcon(":icons/unconfirmed.png")
695             elif conf == 0:
696                 time_str = 'pending'
697                 icon = QIcon(":icons/unconfirmed.png")
698             elif conf < 6:
699                 icon = QIcon(":icons/clock%d.png"%conf)
700             else:
701                 icon = QIcon(":icons/confirmed.png")
702
703             if value is not None:
704                 v_str = self.format_amount(value, True, whitespaces=True)
705             else:
706                 v_str = '--'
707
708             balance_str = self.format_amount(balance, whitespaces=True)
709             
710             if tx_hash:
711                 label, is_default_label = self.wallet.get_label(tx_hash)
712             else:
713                 label = _('Pruned transaction outputs')
714                 is_default_label = False
715
716             item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
717             item.setFont(2, QFont(MONOSPACE_FONT))
718             item.setFont(3, QFont(MONOSPACE_FONT))
719             item.setFont(4, QFont(MONOSPACE_FONT))
720             if value < 0:
721                 item.setForeground(3, QBrush(QColor("#BC1E1E")))
722             if tx_hash:
723                 item.setData(0, Qt.UserRole, tx_hash)
724                 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
725             if is_default_label:
726                 item.setForeground(2, QBrush(QColor('grey')))
727
728             item.setIcon(0, icon)
729             self.history_list.insertTopLevelItem(0,item)
730             
731
732         self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
733
734
735     def create_send_tab(self):
736         w = QWidget()
737
738         grid = QGridLayout()
739         grid.setSpacing(8)
740         grid.setColumnMinimumWidth(3,300)
741         grid.setColumnStretch(5,1)
742
743
744         self.payto_e = QLineEdit()
745         grid.addWidget(QLabel(_('Pay to')), 1, 0)
746         grid.addWidget(self.payto_e, 1, 1, 1, 3)
747             
748         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)
749
750         completer = QCompleter()
751         completer.setCaseSensitivity(False)
752         self.payto_e.setCompleter(completer)
753         completer.setModel(self.completions)
754
755         self.message_e = QLineEdit()
756         grid.addWidget(QLabel(_('Description')), 2, 0)
757         grid.addWidget(self.message_e, 2, 1, 1, 3)
758         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)
759
760         self.pay_from = []
761         grid.addWidget(QLabel(_('Selected\nInputs')), 3, 0)
762         self.from_list = QTreeWidget(self)
763         self.from_list.setColumnCount(2)
764         self.from_list.setColumnWidth(0, 350)
765         self.from_list.setColumnWidth(1, 50)
766         self.from_list.setHeaderHidden (True)
767         self.from_list.setMaximumHeight(80)
768         grid.addWidget(self.from_list, 3, 1, 1, 3)
769         self.connect(self.tabs, SIGNAL('currentChanged(int)'),
770                 lambda: self.update_pay_from_list(grid))
771
772         self.amount_e = AmountEdit(self.base_unit)
773         grid.addWidget(QLabel(_('Amount')), 4, 0)
774         grid.addWidget(self.amount_e, 4, 1)
775         grid.addWidget(HelpButton(
776                 _('Amount to be sent.') + '\n\n' \
777                     + _('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.') \
778                     + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.')), 4, 3)
779         
780         self.fee_e = AmountEdit(self.base_unit)
781         grid.addWidget(QLabel(_('Fee')), 5, 0)
782         grid.addWidget(self.fee_e, 5, 1)
783         grid.addWidget(HelpButton(
784                 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
785                     + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
786                     + _('A suggested fee is automatically added to this field. You may override it. The suggested fee increases with the size of the transaction.')), 5, 3)
787
788
789         self.send_button = EnterButton(_("Send"), self.do_send)
790         grid.addWidget(self.send_button, 6, 1)
791
792         b = EnterButton(_("Clear"),self.do_clear)
793         grid.addWidget(b, 6, 2)
794
795         self.payto_sig = QLabel('')
796         grid.addWidget(self.payto_sig, 7, 0, 1, 4)
797
798         QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
799         QShortcut(QKeySequence("Down"), w, w.focusNextChild)
800         w.setLayout(grid) 
801
802         w2 = QWidget()
803         vbox = QVBoxLayout()
804         vbox.addWidget(w)
805         vbox.addStretch(1)
806         w2.setLayout(vbox)
807
808         def entry_changed( is_fee ):
809             self.funds_error = False
810
811             if self.amount_e.is_shortcut:
812                 self.amount_e.is_shortcut = False
813                 c, u = self.wallet.get_account_balance(self.current_account)
814                 inputs, total, fee = self.wallet.choose_tx_inputs_from_account( c + u, 0, self.current_account)
815                 fee = self.wallet.estimated_fee(inputs)
816                 amount = total - fee
817                 self.amount_e.setText( self.format_amount(amount) )
818                 self.fee_e.setText( self.format_amount( fee ) )
819                 return
820                 
821             amount = self.read_amount(str(self.amount_e.text()))
822             fee = self.read_amount(str(self.fee_e.text()))
823
824             if not is_fee: fee = None
825             if amount is None:
826                 return
827             inputs, total, fee = self.wallet.choose_tx_inputs_from_account( amount, fee, self.current_account )
828             if not is_fee:
829                 self.fee_e.setText( self.format_amount( fee ) )
830             if inputs:
831                 palette = QPalette()
832                 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
833                 text = ""
834             else:
835                 palette = QPalette()
836                 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
837                 self.funds_error = True
838                 text = _( "Not enough funds" )
839                 c, u = self.wallet.get_frozen_balance()
840                 if c+u: text += ' (' + self.format_amount(c+u).strip() + self.base_unit() + ' ' +_("are frozen") + ')'
841
842             self.statusBar().showMessage(text)
843             self.amount_e.setPalette(palette)
844             self.fee_e.setPalette(palette)
845
846         self.amount_e.textChanged.connect(lambda: entry_changed(False) )
847         self.fee_e.textChanged.connect(lambda: entry_changed(True) )
848
849         run_hook('create_send_tab', grid)
850         return w2
851
852
853     def update_pay_from_list(self, grid):
854         self.from_list.clear()
855         grid.itemAtPosition(3,0).widget().setHidden(len(self.pay_from) == 0)
856         grid.itemAtPosition(3,1).widget().setHidden(len(self.pay_from) == 0)
857         for addr in self.pay_from:
858             c, u = self.wallet.get_addr_balance(addr)
859             balance = self.format_amount(c + u)
860             self.from_list.addTopLevelItem(QTreeWidgetItem( [addr, balance] ))
861
862
863     def update_completions(self):
864         l = []
865         for addr,label in self.wallet.labels.items():
866             if addr in self.wallet.addressbook:
867                 l.append( label + '  <' + addr + '>')
868
869         run_hook('update_completions', l)
870         self.completions.setStringList(l)
871
872
873     def protected(func):
874         return lambda s, *args: s.do_protect(func, args)
875
876
877     def do_send(self):
878
879         label = unicode( self.message_e.text() )
880         r = unicode( self.payto_e.text() )
881         r = r.strip()
882
883         # label or alias, with address in brackets
884         m = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
885         to_address = m.group(2) if m else r
886
887         if not is_valid(to_address):
888             QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
889             return
890
891         try:
892             amount = self.read_amount(unicode( self.amount_e.text()))
893         except Exception:
894             QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
895             return
896         try:
897             fee = self.read_amount(unicode( self.fee_e.text()))
898         except Exception:
899             QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
900             return
901
902         confirm_amount = self.config.get('confirm_amount', 100000000)
903         if amount >= confirm_amount:
904             if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : to_address}):
905                 return
906
907         self.send_tx(to_address, amount, fee, label)
908
909
910     @protected
911     def send_tx(self, to_address, amount, fee, label, password):
912
913         try:
914             tx = self.wallet.mktx_from_account( [(to_address, amount)],
915                     password, fee, self.current_account, self.pay_from)
916         except Exception as e:
917             traceback.print_exc(file=sys.stdout)
918             self.show_message(str(e))
919             return
920
921         if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
922             QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
923             return
924
925         if label: 
926             self.wallet.set_label(tx.hash(), label)
927
928         if tx.is_complete:
929             h = self.wallet.send_tx(tx)
930             waiting_dialog(lambda: False if self.wallet.tx_event.isSet() else _("Please wait..."))
931             status, msg = self.wallet.receive_tx( h )
932             if status:
933                 QMessageBox.information(self, '', _('Payment sent.')+'\n'+msg, _('OK'))
934                 self.do_clear()
935                 self.update_contacts_tab()
936             else:
937                 QMessageBox.warning(self, _('Error'), msg, _('OK'))
938         else:
939
940             self.show_transaction(tx)
941
942         # add recipient to addressbook
943         if to_address not in self.wallet.addressbook and not self.wallet.is_mine(to_address):
944             self.wallet.addressbook.append(to_address)
945
946
947
948
949     def set_url(self, url):
950         address, amount, label, message, signature, identity, url = util.parse_url(url)
951
952         try:
953             if amount and self.base_unit() == 'mBTC': amount = str( 1000* Decimal(amount))
954             elif amount: amount = str(Decimal(amount))
955         except Exception:
956             amount = "0.0"
957             QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
958
959         if self.mini:
960             self.mini.set_payment_fields(address, amount)
961
962         if label and self.wallet.labels.get(address) != label:
963             if self.question('Give label "%s" to address %s ?'%(label,address)):
964                 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
965                     self.wallet.addressbook.append(address)
966                 self.wallet.set_label(address, label)
967
968         run_hook('set_url', url, self.show_message, self.question)
969
970         self.tabs.setCurrentIndex(1)
971         label = self.wallet.labels.get(address)
972         m_addr = label + '  <'+ address +'>' if label else address
973         self.payto_e.setText(m_addr)
974
975         self.message_e.setText(message)
976         if amount:
977             self.amount_e.setText(amount)
978
979         if identity:
980             self.set_frozen(self.payto_e,True)
981             self.set_frozen(self.amount_e,True)
982             self.set_frozen(self.message_e,True)
983             self.payto_sig.setText( '      '+_('The bitcoin URI was signed by')+' ' + identity )
984         else:
985             self.payto_sig.setVisible(False)
986
987     def do_clear(self):
988         self.payto_sig.setVisible(False)
989         for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
990             e.setText('')
991             self.set_frozen(e,False)
992
993         self.pay_from = []
994         self.tabs.emit(SIGNAL('currentChanged(int)'), 1)
995
996         self.update_status()
997
998     def set_frozen(self,entry,frozen):
999         if frozen:
1000             entry.setReadOnly(True)
1001             entry.setFrame(False)
1002             palette = QPalette()
1003             palette.setColor(entry.backgroundRole(), QColor('lightgray'))
1004             entry.setPalette(palette)
1005         else:
1006             entry.setReadOnly(False)
1007             entry.setFrame(True)
1008             palette = QPalette()
1009             palette.setColor(entry.backgroundRole(), QColor('white'))
1010             entry.setPalette(palette)
1011
1012
1013     def set_addrs_frozen(self,addrs,freeze):
1014         for addr in addrs:
1015             if not addr: continue
1016             if addr in self.wallet.frozen_addresses and not freeze:
1017                 self.wallet.unfreeze(addr)
1018             elif addr not in self.wallet.frozen_addresses and freeze:
1019                 self.wallet.freeze(addr)
1020         self.update_receive_tab()
1021
1022     def set_addrs_prioritized(self,addrs,prioritize):
1023         for addr in addrs:
1024             if not addr: continue
1025             if addr in self.wallet.prioritized_addresses and not prioritize:
1026                 self.wallet.unprioritize(addr)
1027             elif addr not in self.wallet.prioritized_addresses and prioritize:
1028                 self.wallet.prioritize(addr)
1029         self.update_receive_tab()
1030
1031
1032     def create_list_tab(self, headers):
1033         "generic tab creation method"
1034         l = MyTreeWidget(self)
1035         l.setColumnCount( len(headers) )
1036         l.setHeaderLabels( headers )
1037
1038         w = QWidget()
1039         vbox = QVBoxLayout()
1040         w.setLayout(vbox)
1041
1042         vbox.setMargin(0)
1043         vbox.setSpacing(0)
1044         vbox.addWidget(l)
1045         buttons = QWidget()
1046         vbox.addWidget(buttons)
1047
1048         hbox = QHBoxLayout()
1049         hbox.setMargin(0)
1050         hbox.setSpacing(0)
1051         buttons.setLayout(hbox)
1052
1053         return l,w,hbox
1054
1055
1056     def create_receive_tab(self):
1057         l,w,hbox = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1058         l.setContextMenuPolicy(Qt.CustomContextMenu)
1059         l.customContextMenuRequested.connect(self.create_receive_menu)
1060         l.setSelectionMode(QAbstractItemView.ExtendedSelection)
1061         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1062         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1063         self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
1064         self.receive_list = l
1065         self.receive_buttons_hbox = hbox
1066         hbox.addStretch(1)
1067         return w
1068
1069
1070
1071
1072     def save_column_widths(self):
1073         self.column_widths["receive"] = []
1074         for i in range(self.receive_list.columnCount() -1):
1075             self.column_widths["receive"].append(self.receive_list.columnWidth(i))
1076         
1077         self.column_widths["history"] = []
1078         for i in range(self.history_list.columnCount() - 1):
1079             self.column_widths["history"].append(self.history_list.columnWidth(i))
1080
1081         self.column_widths["contacts"] = []
1082         for i in range(self.contacts_list.columnCount() - 1):
1083             self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1084
1085         self.config.set_key("column_widths_2", self.column_widths, True)
1086
1087
1088     def create_contacts_tab(self):
1089         l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1090         l.setContextMenuPolicy(Qt.CustomContextMenu)
1091         l.customContextMenuRequested.connect(self.create_contact_menu)
1092         for i,width in enumerate(self.column_widths['contacts']):
1093             l.setColumnWidth(i, width)
1094
1095         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1096         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1097         self.contacts_list = l
1098         self.contacts_buttons_hbox = hbox
1099         hbox.addStretch(1)
1100         return w
1101
1102
1103     def delete_imported_key(self, addr):
1104         if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1105             self.wallet.delete_imported_key(addr)
1106             self.update_receive_tab()
1107             self.update_history_tab()
1108
1109     def edit_account_label(self, k):
1110         text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1111         if ok:
1112             label = unicode(text)
1113             self.wallet.set_label(k,label)
1114             self.update_receive_tab()
1115
1116     def account_set_expanded(self, item, k, b):
1117         item.setExpanded(b)
1118         self.accounts_expanded[k] = b
1119
1120     def create_account_menu(self, position, k, item):
1121         menu = QMenu()
1122         if item.isExpanded():
1123             menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1124         else:
1125             menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1126         menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1127         menu.addAction(_("View details"), lambda: self.show_account_details(k))
1128         if self.wallet.account_is_pending(k):
1129             menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1130         menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1131
1132     def delete_pending_account(self, k):
1133         self.wallet.delete_pending_account(k)
1134         self.update_receive_tab()
1135
1136     def create_receive_menu(self, position):
1137         # fixme: this function apparently has a side effect.
1138         # if it is not called the menu pops up several times
1139         #self.receive_list.selectedIndexes() 
1140
1141         selected = self.receive_list.selectedItems()
1142         multi_select = len(selected) > 1
1143         addrs = [unicode(item.text(0)) for item in selected]
1144         if not multi_select:
1145             item = self.receive_list.itemAt(position)
1146             if not item: return
1147
1148             addr = addrs[0]
1149             if not is_valid(addr):
1150                 k = str(item.data(0,32).toString())
1151                 if k:
1152                     self.create_account_menu(position, k, item)
1153                 else:
1154                     item.setExpanded(not item.isExpanded())
1155                 return
1156
1157         menu = QMenu()
1158         if not multi_select:
1159             menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1160             menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")) )
1161             menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1162             if self.wallet.seed:
1163                 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1164                 menu.addAction(_("Sign message"), lambda: self.sign_message(addr))
1165             if addr in self.wallet.imported_keys:
1166                 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1167
1168         if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1169             menu.addAction(_("Freeze"), lambda: self.set_addrs_frozen(addrs, True))
1170         if any(addr in self.wallet.frozen_addresses for addr in addrs):
1171             menu.addAction(_("Unfreeze"), lambda: self.set_addrs_frozen(addrs, False))
1172         if any(addr not in self.wallet.prioritized_addresses for addr in addrs):
1173             menu.addAction(_("Prioritize"),
1174                     lambda: self.set_addrs_prioritized(addrs, True))
1175         if any(addr in self.wallet.prioritized_addresses for addr in addrs):
1176             menu.addAction(_("Unprioritize"),
1177                     lambda: self.set_addrs_prioritized(addrs, False))
1178
1179         total = 0
1180         for item in self.receive_list.selectedItems():
1181             c, u = self.wallet.get_addr_balance(unicode(item.text(0)))
1182             total += c + u
1183         balance = "  [%s]" % self.format_amount(total)
1184         menu.addAction(_("Send From")+balance,
1185                 lambda: self.send_from_addresses(self.receive_list))
1186             
1187         run_hook('receive_menu', menu)
1188         menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1189
1190
1191     def send_from_addresses(self, addrs):
1192         for item in addrs.selectedItems():
1193             self.pay_from.append(unicode(item.text(0)))
1194         self.tabs.setCurrentIndex(1)
1195
1196
1197     def payto(self, addr):
1198         if not addr: return
1199         label = self.wallet.labels.get(addr)
1200         m_addr = label + '  <' + addr + '>' if label else addr
1201         self.tabs.setCurrentIndex(1)
1202         self.payto_e.setText(m_addr)
1203         self.amount_e.setFocus()
1204
1205
1206     def delete_contact(self, x):
1207         if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1208             self.wallet.delete_contact(x)
1209             self.wallet.set_label(x, None)
1210             self.update_history_tab()
1211             self.update_contacts_tab()
1212             self.update_completions()
1213
1214
1215     def create_contact_menu(self, position):
1216         item = self.contacts_list.itemAt(position)
1217         if not item: return
1218         addr = unicode(item.text(0))
1219         label = unicode(item.text(1))
1220         is_editable = item.data(0,32).toBool()
1221         payto_addr = item.data(0,33).toString()
1222         menu = QMenu()
1223         menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1224         menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1225         menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1226         if is_editable:
1227             menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1228             menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1229
1230         run_hook('create_contact_menu', menu, item)
1231         menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1232
1233
1234     def update_receive_item(self, item, num_tx = 0):
1235         item.setFont(0, QFont(MONOSPACE_FONT))
1236         address = str(item.data(0,0).toString())
1237         label = self.wallet.labels.get(address,'')
1238         item.setData(1,0,label)
1239         item.setData(0,32, True) # is editable
1240
1241         run_hook('update_receive_item', address, item)
1242
1243         if not self.wallet.is_mine(address): return
1244
1245         c, u = self.wallet.get_addr_balance(address)
1246         balance = self.format_amount(c + u)
1247         item.setData(2,0,balance)
1248
1249         if (num_tx > 1) and (c == -u):
1250             item.setForeground(0,QColor('lightgray'))
1251             item.setForeground(1,QColor('gray'))
1252             item.setForeground(2,QColor('gray'))
1253             item.setForeground(3,QColor('gray'))
1254         elif address in self.wallet.frozen_addresses: 
1255             item.setBackgroundColor(0, QColor('lightblue'))
1256         elif address in self.wallet.prioritized_addresses: 
1257             item.setBackgroundColor(0, QColor('lightgreen'))
1258
1259
1260     def update_receive_tab(self):
1261         l = self.receive_list
1262         
1263         l.clear()
1264         l.setColumnHidden(2, False)
1265         l.setColumnHidden(3, False)
1266         for i,width in enumerate(self.column_widths['receive']):
1267             l.setColumnWidth(i, width)
1268
1269         if self.current_account is None:
1270             account_items = self.wallet.accounts.items()
1271         elif self.current_account != -1:
1272             account_items = [(self.current_account, self.wallet.accounts.get(self.current_account))]
1273         else:
1274             account_items = []
1275
1276         for k, account in account_items:
1277             name = self.wallet.get_account_name(k)
1278             c,u = self.wallet.get_account_balance(k)
1279             account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1280             l.addTopLevelItem(account_item)
1281             account_item.setExpanded(self.accounts_expanded.get(k, True))
1282             account_item.setData(0, 32, k)
1283
1284             if not self.wallet.is_seeded(k):
1285                 icon = QIcon(":icons/key.png")
1286                 account_item.setIcon(0, icon)
1287             
1288             for is_change in ([0,1]):
1289                 name = _("Receiving") if not is_change else _("Change")
1290                 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1291                 account_item.addChild(seq_item)
1292                 if not is_change: seq_item.setExpanded(True)
1293
1294                 is_red = False
1295                 gap = 0
1296
1297                 for address in account.get_addresses(is_change):
1298                     h = self.wallet.history.get(address,[])
1299             
1300                     if h == []:
1301                         gap += 1
1302                         if gap > self.wallet.gap_limit:
1303                             is_red = True
1304                     else:
1305                         gap = 0
1306
1307                     num_tx = '*' if h == ['*'] else "%d"%len(h)
1308                     item = QTreeWidgetItem( [ address, '', '', num_tx] )
1309                     self.update_receive_item(item, len(h))
1310                     if is_red:
1311                         item.setBackgroundColor(1, QColor('red'))
1312                     seq_item.addChild(item)
1313
1314
1315         for k, addr in self.wallet.get_pending_accounts():
1316             name = self.wallet.labels.get(k,'')
1317             account_item = QTreeWidgetItem( [ name + "  [ "+_('pending account')+" ]", '', '', ''] )
1318             self.update_receive_item(item)
1319             l.addTopLevelItem(account_item)
1320             account_item.setExpanded(True)
1321             account_item.setData(0, 32, k)
1322             item = QTreeWidgetItem( [ addr, '', '', '', ''] )
1323             account_item.addChild(item)
1324             self.update_receive_item(item)
1325
1326
1327         if self.wallet.imported_keys and (self.current_account is None or self.current_account == -1):
1328             c,u = self.wallet.get_imported_balance()
1329             account_item = QTreeWidgetItem( [ _('Imported'), '', self.format_amount(c+u), ''] )
1330             l.addTopLevelItem(account_item)
1331             account_item.setExpanded(True)
1332             for address in self.wallet.imported_keys.keys():
1333                 item = QTreeWidgetItem( [ address, '', '', ''] )
1334                 self.update_receive_item(item)
1335                 account_item.addChild(item)
1336                 
1337
1338         # we use column 1 because column 0 may be hidden
1339         l.setCurrentItem(l.topLevelItem(0),1)
1340
1341
1342     def update_contacts_tab(self):
1343         l = self.contacts_list
1344         l.clear()
1345
1346         for address in self.wallet.addressbook:
1347             label = self.wallet.labels.get(address,'')
1348             n = self.wallet.get_num_tx(address)
1349             item = QTreeWidgetItem( [ address, label, "%d"%n] )
1350             item.setFont(0, QFont(MONOSPACE_FONT))
1351             # 32 = label can be edited (bool)
1352             item.setData(0,32, True)
1353             # 33 = payto string
1354             item.setData(0,33, address)
1355             l.addTopLevelItem(item)
1356
1357         run_hook('update_contacts_tab', l)
1358         l.setCurrentItem(l.topLevelItem(0))
1359
1360
1361
1362     def create_console_tab(self):
1363         from console import Console
1364         self.console = console = Console()
1365         return console
1366
1367
1368     def update_console(self):
1369         console = self.console
1370         console.history = self.config.get("console-history",[])
1371         console.history_index = len(console.history)
1372
1373         console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1374         console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1375
1376         c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1377         methods = {}
1378         def mkfunc(f, method):
1379             return lambda *args: apply( f, (method, args, self.password_dialog ))
1380         for m in dir(c):
1381             if m[0]=='_' or m in ['network','wallet']: continue
1382             methods[m] = mkfunc(c._run, m)
1383             
1384         console.updateNamespace(methods)
1385
1386
1387     def change_account(self,s):
1388         if s == _("All accounts"):
1389             self.current_account = None
1390         else:
1391             accounts = self.wallet.get_account_names()
1392             for k, v in accounts.items():
1393                 if v == s:
1394                     self.current_account = k
1395         self.update_history_tab()
1396         self.update_status()
1397         self.update_receive_tab()
1398
1399     def create_status_bar(self):
1400
1401         sb = QStatusBar()
1402         sb.setFixedHeight(35)
1403         qtVersion = qVersion()
1404
1405         self.balance_label = QLabel("")
1406         sb.addWidget(self.balance_label)
1407
1408         from version_getter import UpdateLabel
1409         self.updatelabel = UpdateLabel(self.config, sb)
1410
1411         self.account_selector = QComboBox()
1412         self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account) 
1413         sb.addPermanentWidget(self.account_selector)
1414
1415         if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1416             sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1417
1418         self.lock_icon = QIcon()
1419         self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1420         sb.addPermanentWidget( self.password_button )
1421             
1422         sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1423         self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog ) 
1424         sb.addPermanentWidget( self.seed_button )
1425         self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog ) 
1426         sb.addPermanentWidget( self.status_button )
1427
1428         run_hook('create_status_bar', (sb,))
1429
1430         self.setStatusBar(sb)
1431
1432
1433     def update_lock_icon(self):
1434         icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1435         self.password_button.setIcon( icon )
1436
1437
1438     def update_buttons_on_seed(self):
1439         if not self.wallet.is_watching_only():
1440            self.seed_button.show()
1441            self.password_button.show()
1442            self.send_button.setText(_("Send"))
1443         else:
1444            self.password_button.hide()
1445            self.seed_button.hide()
1446            self.send_button.setText(_("Create unsigned transaction"))
1447
1448
1449     def change_password_dialog(self):
1450         from password_dialog import PasswordDialog
1451         d = PasswordDialog(self.wallet, self)
1452         d.run()
1453         self.update_lock_icon()
1454
1455
1456     def new_contact_dialog(self):
1457
1458         d = QDialog(self)
1459         vbox = QVBoxLayout(d)
1460         vbox.addWidget(QLabel(_('New Contact')+':'))
1461         
1462         grid = QGridLayout()
1463         line1 = QLineEdit()
1464         line2 = QLineEdit()
1465         grid.addWidget(QLabel(_("Address")), 1, 0)
1466         grid.addWidget(line1, 1, 1)
1467         grid.addWidget(QLabel(_("Name")), 2, 0)
1468         grid.addWidget(line2, 2, 1)
1469
1470         vbox.addLayout(grid)
1471         vbox.addLayout(ok_cancel_buttons(d))
1472     
1473         if not d.exec_():
1474             return
1475         
1476         address = str(line1.text())
1477         label = unicode(line2.text())
1478         
1479         if not is_valid(address):
1480             QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1481             return
1482         
1483         self.wallet.add_contact(address)
1484         if label:
1485             self.wallet.set_label(address, label)
1486
1487         self.update_contacts_tab()
1488         self.update_history_tab()
1489         self.update_completions()
1490         self.tabs.setCurrentIndex(3)
1491
1492
1493     def new_account_dialog(self):
1494
1495         dialog = QDialog(self)
1496         dialog.setModal(1)
1497         dialog.setWindowTitle(_("New Account"))
1498
1499         vbox = QVBoxLayout()
1500         vbox.addWidget(QLabel(_('Account name')+':'))
1501         e = QLineEdit()
1502         vbox.addWidget(e)
1503         msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1504             + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1505         l = QLabel(msg)
1506         l.setWordWrap(True)
1507         vbox.addWidget(l)
1508
1509         vbox.addLayout(ok_cancel_buttons(dialog))
1510         dialog.setLayout(vbox)
1511         r = dialog.exec_()
1512         if not r: return
1513
1514         name = str(e.text())
1515         if not name: return
1516
1517         self.wallet.create_pending_account('1', name)
1518         self.update_receive_tab()
1519         self.tabs.setCurrentIndex(2)
1520         
1521             
1522
1523     def show_master_public_key_old(self):
1524         dialog = QDialog(self)
1525         dialog.setModal(1)
1526         dialog.setWindowTitle(_("Master Public Key"))
1527
1528         main_text = QTextEdit()
1529         main_text.setText(self.wallet.get_master_public_key())
1530         main_text.setReadOnly(True)
1531         main_text.setMaximumHeight(170)
1532         qrw = QRCodeWidget(self.wallet.get_master_public_key())
1533
1534         ok_button = QPushButton(_("OK"))
1535         ok_button.setDefault(True)
1536         ok_button.clicked.connect(dialog.accept)
1537
1538         main_layout = QGridLayout()
1539         main_layout.addWidget(QLabel(_('Your Master Public Key is:')), 0, 0, 1, 2)
1540
1541         main_layout.addWidget(main_text, 1, 0)
1542         main_layout.addWidget(qrw, 1, 1 )
1543
1544         vbox = QVBoxLayout()
1545         vbox.addLayout(main_layout)
1546         vbox.addLayout(close_button(dialog))
1547         dialog.setLayout(vbox)
1548         dialog.exec_()
1549         
1550
1551     def show_master_public_key(self):
1552
1553         if self.wallet.seed_version == 4:
1554             self.show_master_public_key_old()
1555             return
1556
1557         dialog = QDialog(self)
1558         dialog.setModal(1)
1559         dialog.setWindowTitle(_("Master Public Keys"))
1560
1561         chain_text = QTextEdit()
1562         chain_text.setReadOnly(True)
1563         chain_text.setMaximumHeight(170)
1564         chain_qrw = QRCodeWidget()
1565
1566         mpk_text = QTextEdit()
1567         mpk_text.setReadOnly(True)
1568         mpk_text.setMaximumHeight(170)
1569         mpk_qrw = QRCodeWidget()
1570
1571         main_layout = QGridLayout()
1572
1573         main_layout.addWidget(QLabel(_('Key')), 1, 0)
1574         main_layout.addWidget(mpk_text, 1, 1)
1575         main_layout.addWidget(mpk_qrw, 1, 2)
1576
1577         main_layout.addWidget(QLabel(_('Chain')), 2, 0)
1578         main_layout.addWidget(chain_text, 2, 1)
1579         main_layout.addWidget(chain_qrw, 2, 2)
1580
1581         def update(key):
1582             c, K, cK = self.wallet.master_public_keys[str(key)]
1583             chain_text.setText(c)
1584             chain_qrw.set_addr(c)
1585             chain_qrw.update_qr()
1586             mpk_text.setText(K)
1587             mpk_qrw.set_addr(K)
1588             mpk_qrw.update_qr()
1589             
1590         key_selector = QComboBox()
1591         keys = sorted(self.wallet.master_public_keys.keys())
1592         key_selector.addItems(keys)
1593
1594         main_layout.addWidget(QLabel(_('Derivation:')), 0, 0)
1595         main_layout.addWidget(key_selector, 0, 1)
1596         dialog.connect(key_selector,SIGNAL("activated(QString)"),update) 
1597
1598         update(keys[0])
1599
1600         vbox = QVBoxLayout()
1601         vbox.addLayout(main_layout)
1602         vbox.addLayout(close_button(dialog))
1603
1604         dialog.setLayout(vbox)
1605         dialog.exec_()
1606         
1607
1608     @protected
1609     def show_seed_dialog(self, password):
1610         if self.wallet.is_watching_only():
1611             QMessageBox.information(self, _('Message'), _('This is a watching-only wallet'), _('OK'))
1612             return
1613
1614         if self.wallet.seed:
1615             try:
1616                 mnemonic = self.wallet.get_mnemonic(password)
1617             except Exception:
1618                 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1619                 return
1620             from seed_dialog import SeedDialog
1621             d = SeedDialog(self, mnemonic, self.wallet.imported_keys)
1622             d.exec_()
1623         else:
1624             l = {}
1625             for k in self.wallet.master_private_keys.keys():
1626                 pk = self.wallet.get_master_private_key(k, password)
1627                 l[k] = pk
1628             from seed_dialog import PrivateKeysDialog
1629             d = PrivateKeysDialog(self,l)
1630             d.exec_()
1631
1632
1633
1634
1635
1636     def show_qrcode(self, data, title = _("QR code")):
1637         if not data: return
1638         d = QDialog(self)
1639         d.setModal(1)
1640         d.setWindowTitle(title)
1641         d.setMinimumSize(270, 300)
1642         vbox = QVBoxLayout()
1643         qrw = QRCodeWidget(data)
1644         vbox.addWidget(qrw, 1)
1645         vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1646         hbox = QHBoxLayout()
1647         hbox.addStretch(1)
1648
1649         filename = os.path.join(self.config.path, "qrcode.bmp")
1650
1651         def print_qr():
1652             bmp.save_qrcode(qrw.qr, filename)
1653             QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1654
1655         def copy_to_clipboard():
1656             bmp.save_qrcode(qrw.qr, filename)
1657             self.app.clipboard().setImage(QImage(filename))
1658             QMessageBox.information(None, _('Message'), _("QR code saved to clipboard"), _('OK'))
1659
1660         b = QPushButton(_("Copy"))
1661         hbox.addWidget(b)
1662         b.clicked.connect(copy_to_clipboard)
1663
1664         b = QPushButton(_("Save"))
1665         hbox.addWidget(b)
1666         b.clicked.connect(print_qr)
1667
1668         b = QPushButton(_("Close"))
1669         hbox.addWidget(b)
1670         b.clicked.connect(d.accept)
1671         b.setDefault(True)
1672
1673         vbox.addLayout(hbox)
1674         d.setLayout(vbox)
1675         d.exec_()
1676
1677
1678     def do_protect(self, func, args):
1679         if self.wallet.use_encryption:
1680             password = self.password_dialog()
1681             if not password:
1682                 return
1683         else:
1684             password = None
1685             
1686         if args != (False,):
1687             args = (self,) + args + (password,)
1688         else:
1689             args = (self,password)
1690         apply( func, args)
1691
1692
1693     @protected
1694     def show_private_key(self, address, password):
1695         if not address: return
1696         try:
1697             pk_list = self.wallet.get_private_key(address, password)
1698         except Exception as e:
1699             self.show_message(str(e))
1700             return
1701         QMessageBox.information(self, _('Private key'), _('Address')+ ': ' + address + '\n\n' + _('Private key') + ': ' + '\n'.join(pk_list), _('OK'))
1702
1703
1704     @protected
1705     def do_sign(self, address, message, signature, password):
1706         message = unicode(message.toPlainText())
1707         message = message.encode('utf-8')
1708         try:
1709             sig = self.wallet.sign_message(str(address.text()), message, password)
1710             signature.setText(sig)
1711         except Exception as e:
1712             self.show_message(str(e))
1713
1714     def sign_message(self, address):
1715         if not address: return
1716         d = QDialog(self)
1717         d.setModal(1)
1718         d.setWindowTitle(_('Sign Message'))
1719         d.setMinimumSize(410, 290)
1720
1721         tab_widget = QTabWidget()
1722         tab = QWidget()
1723         layout = QGridLayout(tab)
1724
1725         sign_address = QLineEdit()
1726
1727         sign_address.setText(address)
1728         layout.addWidget(QLabel(_('Address')), 1, 0)
1729         layout.addWidget(sign_address, 1, 1)
1730
1731         sign_message = QTextEdit()
1732         layout.addWidget(QLabel(_('Message')), 2, 0)
1733         layout.addWidget(sign_message, 2, 1)
1734         layout.setRowStretch(2,3)
1735
1736         sign_signature = QTextEdit()
1737         layout.addWidget(QLabel(_('Signature')), 3, 0)
1738         layout.addWidget(sign_signature, 3, 1)
1739         layout.setRowStretch(3,1)
1740
1741
1742         hbox = QHBoxLayout()
1743         b = QPushButton(_("Sign"))
1744         hbox.addWidget(b)
1745         b.clicked.connect(lambda: self.do_sign(sign_address, sign_message, sign_signature))
1746         b = QPushButton(_("Close"))
1747         b.clicked.connect(d.accept)
1748         hbox.addWidget(b)
1749         layout.addLayout(hbox, 4, 1)
1750         tab_widget.addTab(tab, _("Sign"))
1751
1752
1753         tab = QWidget()
1754         layout = QGridLayout(tab)
1755
1756         verify_address = QLineEdit()
1757         layout.addWidget(QLabel(_('Address')), 1, 0)
1758         layout.addWidget(verify_address, 1, 1)
1759
1760         verify_message = QTextEdit()
1761         layout.addWidget(QLabel(_('Message')), 2, 0)
1762         layout.addWidget(verify_message, 2, 1)
1763         layout.setRowStretch(2,3)
1764
1765         verify_signature = QTextEdit()
1766         layout.addWidget(QLabel(_('Signature')), 3, 0)
1767         layout.addWidget(verify_signature, 3, 1)
1768         layout.setRowStretch(3,1)
1769
1770         def do_verify():
1771             message = unicode(verify_message.toPlainText())
1772             message = message.encode('utf-8')
1773             if bitcoin.verify_message(verify_address.text(), str(verify_signature.toPlainText()), message):
1774                 self.show_message(_("Signature verified"))
1775             else:
1776                 self.show_message(_("Error: wrong signature"))
1777
1778         hbox = QHBoxLayout()
1779         b = QPushButton(_("Verify"))
1780         b.clicked.connect(do_verify)
1781         hbox.addWidget(b)
1782         b = QPushButton(_("Close"))
1783         b.clicked.connect(d.accept)
1784         hbox.addWidget(b)
1785         layout.addLayout(hbox, 4, 1)
1786         tab_widget.addTab(tab, _("Verify"))
1787
1788         vbox = QVBoxLayout()
1789         vbox.addWidget(tab_widget)
1790         d.setLayout(vbox)
1791         d.exec_()
1792
1793         
1794
1795
1796     def question(self, msg):
1797         return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1798
1799     def show_message(self, msg):
1800         QMessageBox.information(self, _('Message'), msg, _('OK'))
1801
1802     def password_dialog(self ):
1803         d = QDialog(self)
1804         d.setModal(1)
1805
1806         pw = QLineEdit()
1807         pw.setEchoMode(2)
1808
1809         vbox = QVBoxLayout()
1810         msg = _('Please enter your password')
1811         vbox.addWidget(QLabel(msg))
1812
1813         grid = QGridLayout()
1814         grid.setSpacing(8)
1815         grid.addWidget(QLabel(_('Password')), 1, 0)
1816         grid.addWidget(pw, 1, 1)
1817         vbox.addLayout(grid)
1818
1819         vbox.addLayout(ok_cancel_buttons(d))
1820         d.setLayout(vbox)
1821
1822         run_hook('password_dialog', pw, grid, 1)
1823         if not d.exec_(): return
1824         return unicode(pw.text())
1825
1826
1827
1828
1829
1830
1831
1832
1833     def tx_from_text(self, txt):
1834         "json or raw hexadecimal"
1835         try:
1836             txt.decode('hex')
1837             tx = Transaction(txt)
1838             return tx
1839         except Exception:
1840             pass
1841
1842         try:
1843             tx_dict = json.loads(str(txt))
1844             assert "hex" in tx_dict.keys()
1845             assert "complete" in tx_dict.keys()
1846             tx = Transaction(tx_dict["hex"], tx_dict["complete"])
1847             if not tx_dict["complete"]:
1848                 assert "input_info" in tx_dict.keys()
1849                 input_info = json.loads(tx_dict['input_info'])
1850                 tx.add_input_info(input_info)
1851             return tx
1852         except Exception:
1853             pass
1854         
1855         QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
1856
1857
1858
1859     def read_tx_from_file(self):
1860         fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1861         if not fileName:
1862             return
1863         try:
1864             with open(fileName, "r") as f:
1865                 file_content = f.read()
1866         except (ValueError, IOError, os.error), reason:
1867             QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1868
1869         return self.tx_from_text(file_content)
1870
1871
1872     @protected
1873     def sign_raw_transaction(self, tx, input_info, password):
1874         self.wallet.signrawtransaction(tx, input_info, [], password)
1875
1876     def do_process_from_text(self):
1877         text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1878         if not text:
1879             return
1880         tx = self.tx_from_text(text)
1881         if tx:
1882             self.show_transaction(tx)
1883
1884     def do_process_from_file(self):
1885         tx = self.read_tx_from_file()
1886         if tx:
1887             self.show_transaction(tx)
1888
1889     def do_process_from_csvReader(self, csvReader):
1890         outputs = []
1891         try:
1892             for row in csvReader:
1893                 address = row[0]
1894                 amount = float(row[1])
1895                 amount = int(100000000*amount)
1896                 outputs.append((address, amount))
1897         except (ValueError, IOError, os.error), reason:
1898             QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1899             return
1900
1901         try:
1902             tx = self.wallet.make_unsigned_transaction(outputs, None, None)
1903         except Exception as e:
1904             self.show_message(str(e))
1905             return
1906
1907         self.show_transaction(tx)
1908
1909     def do_process_from_csv_file(self):
1910         fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
1911         if not fileName:
1912             return
1913         try:
1914             with open(fileName, "r") as f:
1915                 csvReader = csv.reader(f)
1916                 self.do_process_from_csvReader(csvReader)
1917         except (ValueError, IOError, os.error), reason:
1918             QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1919             return
1920
1921     def do_process_from_csv_text(self):
1922         text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
1923                                + _("Format: address, amount. One output per line"), _("Load CSV"))
1924         if not text:
1925             return
1926         f = StringIO.StringIO(text)
1927         csvReader = csv.reader(f)
1928         self.do_process_from_csvReader(csvReader)
1929
1930
1931
1932     @protected
1933     def do_export_privkeys(self, password):
1934         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.")))
1935
1936         try:
1937             select_export = _('Select file to export your private keys to')
1938             fileName = self.getSaveFileName(select_export, 'electrum-private-keys.csv', "*.csv")
1939             if fileName:
1940                 with open(fileName, "w+") as csvfile:
1941                     transaction = csv.writer(csvfile)
1942                     transaction.writerow(["address", "private_key"])
1943
1944                     addresses = self.wallet.addresses(True)
1945                     
1946                     for addr in addresses:
1947                         pk = "".join(self.wallet.get_private_key(addr, password))
1948                         transaction.writerow(["%34s"%addr,pk])
1949
1950                     self.show_message(_("Private keys exported."))
1951
1952         except (IOError, os.error), reason:
1953             export_error_label = _("Electrum was unable to produce a private key-export.")
1954             QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
1955
1956         except Exception as e:
1957           self.show_message(str(e))
1958           return
1959
1960
1961     def do_import_labels(self):
1962         labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
1963         if not labelsFile: return
1964         try:
1965             f = open(labelsFile, 'r')
1966             data = f.read()
1967             f.close()
1968             for key, value in json.loads(data).items():
1969                 self.wallet.set_label(key, value)
1970             QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
1971         except (IOError, os.error), reason:
1972             QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
1973             
1974
1975     def do_export_labels(self):
1976         labels = self.wallet.labels
1977         try:
1978             fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
1979             if fileName:
1980                 with open(fileName, 'w+') as f:
1981                     json.dump(labels, f)
1982                 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
1983         except (IOError, os.error), reason:
1984             QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
1985
1986
1987     def do_export_history(self):
1988         from lite_window import csv_transaction
1989         csv_transaction(self.wallet)
1990
1991
1992     @protected
1993     def do_import_privkey(self, password):
1994         if not self.wallet.imported_keys:
1995             r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
1996                                          + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
1997                                          + _('Are you sure you understand what you are doing?'), 3, 4)
1998             if r == 4: return
1999
2000         text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
2001         if not text: return
2002
2003         text = str(text).split()
2004         badkeys = []
2005         addrlist = []
2006         for key in text:
2007             try:
2008                 addr = self.wallet.import_key(key, password)
2009             except Exception as e:
2010                 badkeys.append(key)
2011                 continue
2012             if not addr: 
2013                 badkeys.append(key)
2014             else:
2015                 addrlist.append(addr)
2016         if addrlist:
2017             QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
2018         if badkeys:
2019             QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
2020         self.update_receive_tab()
2021         self.update_history_tab()
2022
2023
2024     def settings_dialog(self):
2025         d = QDialog(self)
2026         d.setWindowTitle(_('Electrum Settings'))
2027         d.setModal(1)
2028         vbox = QVBoxLayout()
2029         grid = QGridLayout()
2030         grid.setColumnStretch(0,1)
2031
2032         nz_label = QLabel(_('Display zeros') + ':')
2033         grid.addWidget(nz_label, 0, 0)
2034         nz_e = AmountEdit(None,True)
2035         nz_e.setText("%d"% self.num_zeros)
2036         grid.addWidget(nz_e, 0, 1)
2037         msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
2038         grid.addWidget(HelpButton(msg), 0, 2)
2039         if not self.config.is_modifiable('num_zeros'):
2040             for w in [nz_e, nz_label]: w.setEnabled(False)
2041         
2042         lang_label=QLabel(_('Language') + ':')
2043         grid.addWidget(lang_label, 1, 0)
2044         lang_combo = QComboBox()
2045         from electrum.i18n import languages
2046         lang_combo.addItems(languages.values())
2047         try:
2048             index = languages.keys().index(self.config.get("language",''))
2049         except Exception:
2050             index = 0
2051         lang_combo.setCurrentIndex(index)
2052         grid.addWidget(lang_combo, 1, 1)
2053         grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
2054         if not self.config.is_modifiable('language'):
2055             for w in [lang_combo, lang_label]: w.setEnabled(False)
2056
2057         
2058         fee_label = QLabel(_('Transaction fee') + ':')
2059         grid.addWidget(fee_label, 2, 0)
2060         fee_e = AmountEdit(self.base_unit)
2061         fee_e.setText(self.format_amount(self.wallet.fee).strip())
2062         grid.addWidget(fee_e, 2, 1)
2063         msg = _('Fee per kilobyte of transaction.') + ' ' \
2064             + _('Recommended value') + ': ' + self.format_amount(20000)
2065         grid.addWidget(HelpButton(msg), 2, 2)
2066         if not self.config.is_modifiable('fee_per_kb'):
2067             for w in [fee_e, fee_label]: w.setEnabled(False)
2068
2069         units = ['BTC', 'mBTC']
2070         unit_label = QLabel(_('Base unit') + ':')
2071         grid.addWidget(unit_label, 3, 0)
2072         unit_combo = QComboBox()
2073         unit_combo.addItems(units)
2074         unit_combo.setCurrentIndex(units.index(self.base_unit()))
2075         grid.addWidget(unit_combo, 3, 1)
2076         grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2077                                              + '\n1BTC=1000mBTC.\n' \
2078                                              + _(' This settings affects the fields in the Send tab')+' '), 3, 2)
2079
2080         usechange_cb = QCheckBox(_('Use change addresses'))
2081         usechange_cb.setChecked(self.wallet.use_change)
2082         grid.addWidget(usechange_cb, 4, 0)
2083         grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2084         if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2085
2086         grid.setRowStretch(5,1)
2087
2088         vbox.addLayout(grid)
2089         vbox.addLayout(ok_cancel_buttons(d))
2090         d.setLayout(vbox) 
2091
2092         # run the dialog
2093         if not d.exec_(): return
2094
2095         fee = unicode(fee_e.text())
2096         try:
2097             fee = self.read_amount(fee)
2098         except Exception:
2099             QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2100             return
2101
2102         self.wallet.set_fee(fee)
2103         
2104         nz = unicode(nz_e.text())
2105         try:
2106             nz = int( nz )
2107             if nz>8: nz=8
2108         except Exception:
2109             QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2110             return
2111
2112         if self.num_zeros != nz:
2113             self.num_zeros = nz
2114             self.config.set_key('num_zeros', nz, True)
2115             self.update_history_tab()
2116             self.update_receive_tab()
2117
2118         usechange_result = usechange_cb.isChecked()
2119         if self.wallet.use_change != usechange_result:
2120             self.wallet.use_change = usechange_result
2121             self.wallet.storage.put('use_change', self.wallet.use_change)
2122         
2123         unit_result = units[unit_combo.currentIndex()]
2124         if self.base_unit() != unit_result:
2125             self.decimal_point = 8 if unit_result == 'BTC' else 5
2126             self.config.set_key('decimal_point', self.decimal_point, True)
2127             self.update_history_tab()
2128             self.update_status()
2129         
2130         need_restart = False
2131
2132         lang_request = languages.keys()[lang_combo.currentIndex()]
2133         if lang_request != self.config.get('language'):
2134             self.config.set_key("language", lang_request, True)
2135             need_restart = True
2136             
2137         run_hook('close_settings_dialog')
2138
2139         if need_restart:
2140             QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2141
2142
2143     def run_network_dialog(self):
2144         if not self.network:
2145             return
2146         NetworkDialog(self.wallet.network, self.config, self).do_exec()
2147
2148     def closeEvent(self, event):
2149         g = self.geometry()
2150         self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
2151         self.save_column_widths()
2152         self.config.set_key("console-history", self.console.history[-50:], True)
2153         self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2154         event.accept()
2155
2156
2157
2158     def plugins_dialog(self):
2159         from electrum.plugins import plugins
2160
2161         d = QDialog(self)
2162         d.setWindowTitle(_('Electrum Plugins'))
2163         d.setModal(1)
2164
2165         vbox = QVBoxLayout(d)
2166
2167         # plugins
2168         scroll = QScrollArea()
2169         scroll.setEnabled(True)
2170         scroll.setWidgetResizable(True)
2171         scroll.setMinimumSize(400,250)
2172         vbox.addWidget(scroll)
2173
2174         w = QWidget()
2175         scroll.setWidget(w)
2176         w.setMinimumHeight(len(plugins)*35)
2177
2178         grid = QGridLayout()
2179         grid.setColumnStretch(0,1)
2180         w.setLayout(grid)
2181
2182         def do_toggle(cb, p, w):
2183             r = p.toggle()
2184             cb.setChecked(r)
2185             if w: w.setEnabled(r)
2186
2187         def mk_toggle(cb, p, w):
2188             return lambda: do_toggle(cb,p,w)
2189
2190         for i, p in enumerate(plugins):
2191             try:
2192                 cb = QCheckBox(p.fullname())
2193                 cb.setDisabled(not p.is_available())
2194                 cb.setChecked(p.is_enabled())
2195                 grid.addWidget(cb, i, 0)
2196                 if p.requires_settings():
2197                     w = p.settings_widget(self)
2198                     w.setEnabled( p.is_enabled() )
2199                     grid.addWidget(w, i, 1)
2200                 else: 
2201                     w = None
2202                 cb.clicked.connect(mk_toggle(cb,p,w))
2203                 grid.addWidget(HelpButton(p.description()), i, 2)
2204             except Exception:
2205                 print_msg(_("Error: cannot display plugin"), p)
2206                 traceback.print_exc(file=sys.stdout)
2207         grid.setRowStretch(i+1,1)
2208
2209         vbox.addLayout(close_button(d))
2210
2211         d.exec_()
2212
2213
2214     def show_account_details(self, k):
2215         d = QDialog(self)
2216         d.setWindowTitle(_('Account Details'))
2217         d.setModal(1)
2218
2219         vbox = QVBoxLayout(d)
2220         roots = self.wallet.get_roots(k)
2221
2222         name = self.wallet.get_account_name(k)
2223         label = QLabel('Name: ' + name)
2224         vbox.addWidget(label)
2225
2226         acctype = '2 of 2' if len(roots) == 2 else '2 of 3' if len(roots) == 3 else 'Single key'
2227         vbox.addWidget(QLabel('Type: ' + acctype))
2228
2229         label = QLabel('Derivation: ' + k)
2230         vbox.addWidget(label)
2231
2232         #for root in roots:
2233         #    mpk = self.wallet.master_public_keys[root]
2234         #    text = QTextEdit()
2235         #    text.setReadOnly(True)
2236         #    text.setMaximumHeight(120)
2237         #    text.setText(repr(mpk))
2238         #    vbox.addWidget(text)
2239
2240         vbox.addLayout(close_button(d))
2241         d.exec_()