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