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