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