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