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