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