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