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