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