do the banner request in interface, not in wallet
[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
1051         self.run_hook('update_receive_item', address, item)
1052                 
1053         c, u = self.wallet.get_addr_balance(address)
1054         balance = format_satoshis( c + u, False, self.wallet.num_zeros )
1055         item.setData(2,0,balance)
1056
1057         if self.expert_mode:
1058             if address in self.wallet.frozen_addresses: 
1059                 item.setBackgroundColor(0, QColor('lightblue'))
1060             elif address in self.wallet.prioritized_addresses: 
1061                 item.setBackgroundColor(0, QColor('lightgreen'))
1062         
1063
1064     def update_receive_tab(self):
1065         l = self.receive_list
1066         
1067         l.clear()
1068         l.setColumnHidden(2, not self.expert_mode)
1069         l.setColumnHidden(3, not self.expert_mode)
1070         if not self.expert_mode:
1071             width = self.column_widths['receive'][0][0]
1072             l.setColumnWidth(0, width)
1073         else:
1074             for i,width in enumerate(self.column_widths['receive'][self.expert_mode]):
1075                 l.setColumnWidth(i, width)        
1076
1077
1078         for k, account in self.wallet.accounts.items():
1079             name = account.get('name',str(k))
1080             c,u = self.wallet.get_account_balance(k)
1081             account_item = QTreeWidgetItem( [ name, '', format_satoshis(c+u), ''] )
1082             l.addTopLevelItem(account_item)
1083             account_item.setExpanded(True)
1084             
1085
1086             for is_change in [0,1]:
1087                 name = "Receiving" if not is_change else "Change"
1088                 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1089                 account_item.addChild(seq_item)
1090                 if not is_change: seq_item.setExpanded(True)
1091                 is_red = False
1092                 gap = 0
1093
1094                 for address in account[is_change]:
1095                     h = self.wallet.history.get(address,[])
1096             
1097                     if h == []:
1098                         gap += 1
1099                         if gap > self.wallet.gap_limit:
1100                             is_red = True
1101                     else:
1102                         gap = 0
1103
1104                     num_tx = '*' if h == ['*'] else "%d"%len(h)
1105                     item = QTreeWidgetItem( [ address, '', '', num_tx] )
1106                     self.update_receive_item(item)
1107                     if is_red:
1108                         item.setBackgroundColor(1, QColor('red'))
1109                     seq_item.addChild(item)
1110
1111         if self.wallet.imported_keys:
1112             c,u = self.wallet.get_imported_balance()
1113             account_item = QTreeWidgetItem( [ _('Imported'), '', format_satoshis(c+u), ''] )
1114             l.addTopLevelItem(account_item)
1115             account_item.setExpanded(True)
1116             for address in self.wallet.imported_keys.keys():
1117                 item = QTreeWidgetItem( [ address, '', '', ''] )
1118                 self.update_receive_item(item)
1119                 account_item.addChild(item)
1120                 
1121
1122         # we use column 1 because column 0 may be hidden
1123         l.setCurrentItem(l.topLevelItem(0),1)
1124
1125
1126     def update_contacts_tab(self):
1127
1128         l = self.contacts_list
1129         l.clear()
1130
1131         for address in self.wallet.addressbook:
1132             label = self.wallet.labels.get(address,'')
1133             n = self.wallet.get_num_tx(address)
1134             item = QTreeWidgetItem( [ address, label, "%d"%n] )
1135             item.setFont(0, QFont(MONOSPACE_FONT))
1136             # 32 = label can be edited (bool)
1137             item.setData(0,32, True)
1138             # 33 = payto string
1139             item.setData(0,33, address)
1140             l.addTopLevelItem(item)
1141
1142         self.run_hook('update_contacts_tab', l)
1143         l.setCurrentItem(l.topLevelItem(0))
1144
1145
1146
1147     def create_console_tab(self):
1148         from qt_console import Console
1149         self.console = console = Console()
1150         self.console.history = self.config.get("console-history",[])
1151         self.console.history_index = len(self.console.history)
1152
1153         console.updateNamespace({'wallet' : self.wallet, 'interface' : self.wallet.interface, 'gui':self})
1154         console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1155
1156         c = commands.Commands(self.wallet, self.wallet.interface, lambda: self.console.set_json(True))
1157         methods = {}
1158         def mkfunc(f, method):
1159             return lambda *args: apply( f, (method, args, self.password_dialog ))
1160         for m in dir(c):
1161             if m[0]=='_' or m=='wallet' or m == 'interface': continue
1162             methods[m] = mkfunc(c._run, m)
1163             
1164         console.updateNamespace(methods)
1165         return console
1166
1167
1168     def create_status_bar(self):
1169         self.status_text = ""
1170         sb = QStatusBar()
1171         sb.setFixedHeight(35)
1172         qtVersion = qVersion()
1173
1174         update_notification = UpdateLabel(self.config)
1175         if(update_notification.new_version):
1176             sb.addPermanentWidget(update_notification)
1177
1178         if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1179             sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1180         if self.wallet.seed:
1181             self.lock_icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1182             self.password_button = StatusBarButton( self.lock_icon, _("Password"), lambda: self.change_password_dialog(self.wallet, self) )
1183             sb.addPermanentWidget( self.password_button )
1184         sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1185         if self.wallet.seed:
1186             sb.addPermanentWidget( StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog ) )
1187         self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), lambda: self.network_dialog(self.wallet, self) ) 
1188         sb.addPermanentWidget( self.status_button )
1189
1190         self.run_hook('create_status_bar', (sb,))
1191
1192         self.setStatusBar(sb)
1193         
1194     def go_lite(self):
1195         import gui_lite
1196         self.config.set_key('gui', 'lite', True)
1197         self.hide()
1198         if self.lite:
1199             self.lite.mini.show()
1200         else:
1201             self.lite = gui_lite.ElectrumGui(self.wallet, self.config, self)
1202             self.lite.main(None)
1203
1204     def new_contact_dialog(self):
1205         text, ok = QInputDialog.getText(self, _('New Contact'), _('Address') + ':')
1206         address = unicode(text)
1207         if ok:
1208             if is_valid(address):
1209                 self.wallet.addressbook.append(address)
1210                 self.wallet.save()
1211                 self.update_contacts_tab()
1212                 self.update_history_tab()
1213                 self.update_completions()
1214             else:
1215                 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1216
1217     def show_master_public_key(self):
1218         dialog = QDialog(self)
1219         dialog.setModal(1)
1220         dialog.setWindowTitle(_("Master Public Key"))
1221
1222         main_text = QTextEdit()
1223         main_text.setText(self.wallet.get_master_public_key())
1224         main_text.setReadOnly(True)
1225         main_text.setMaximumHeight(170)
1226         qrw = QRCodeWidget(self.wallet.get_master_public_key())
1227
1228         ok_button = QPushButton(_("OK"))
1229         ok_button.setDefault(True)
1230         ok_button.clicked.connect(dialog.accept)
1231
1232         main_layout = QGridLayout()
1233         main_layout.addWidget(QLabel(_('Your Master Public Key is:')), 0, 0, 1, 2)
1234
1235         main_layout.addWidget(main_text, 1, 0)
1236         main_layout.addWidget(qrw, 1, 1 )
1237
1238         vbox = QVBoxLayout()
1239         vbox.addLayout(main_layout)
1240         hbox = QHBoxLayout()
1241         hbox.addStretch(1)
1242         hbox.addWidget(ok_button)
1243         vbox.addLayout(hbox)
1244
1245         dialog.setLayout(vbox)
1246         dialog.exec_()
1247         
1248
1249     @protected
1250     def show_seed_dialog(self, password):
1251         if not self.wallet.seed:
1252             QMessageBox.information(parent, _('Message'), _('No seed'), _('OK'))
1253             return
1254         try:
1255             seed = self.wallet.decode_seed(password)
1256         except:
1257             QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1258             return
1259         self.show_seed(seed, self)
1260
1261
1262     @classmethod
1263     def show_seed(self, seed, parent=None):
1264         dialog = QDialog(parent)
1265         dialog.setModal(1)
1266         dialog.setWindowTitle('Electrum' + ' - ' + _('Seed'))
1267
1268         brainwallet = ' '.join(mnemonic.mn_encode(seed))
1269
1270         label1 = QLabel(_("Your wallet generation seed is")+ ":")
1271
1272         seed_text = QTextEdit(brainwallet)
1273         seed_text.setReadOnly(True)
1274         seed_text.setMaximumHeight(130)
1275         
1276         msg2 =  _("Please write down or memorize these 12 words (order is important).") + " " \
1277               + _("This seed will allow you to recover your wallet in case of computer failure.") + " " \
1278               + _("Your seed is also displayed as QR code, in case you want to transfer it to a mobile phone.") + "<p>" \
1279               + "<b>"+_("WARNING")+":</b> " + _("Never disclose your seed. Never type it on a website.") + "</b><p>"
1280         label2 = QLabel(msg2)
1281         label2.setWordWrap(True)
1282
1283         logo = QLabel()
1284         logo.setPixmap(QPixmap(":icons/seed.png").scaledToWidth(56))
1285         logo.setMaximumWidth(60)
1286
1287         qrw = QRCodeWidget(seed)
1288
1289         ok_button = QPushButton(_("OK"))
1290         ok_button.setDefault(True)
1291         ok_button.clicked.connect(dialog.accept)
1292
1293         grid = QGridLayout()
1294         #main_layout.addWidget(logo, 0, 0)
1295
1296         grid.addWidget(logo, 0, 0)
1297         grid.addWidget(label1, 0, 1)
1298
1299         grid.addWidget(seed_text, 1, 0, 1, 2)
1300
1301         grid.addWidget(qrw, 0, 2, 2, 1)
1302
1303         vbox = QVBoxLayout()
1304         vbox.addLayout(grid)
1305         vbox.addWidget(label2)
1306
1307         hbox = QHBoxLayout()
1308         hbox.addStretch(1)
1309         hbox.addWidget(ok_button)
1310         vbox.addLayout(hbox)
1311
1312         dialog.setLayout(vbox)
1313         dialog.exec_()
1314
1315     def show_qrcode(self, data, title = "QR code"):
1316         if not data: return
1317         d = QDialog(self)
1318         d.setModal(1)
1319         d.setWindowTitle(title)
1320         d.setMinimumSize(270, 300)
1321         vbox = QVBoxLayout()
1322         qrw = QRCodeWidget(data)
1323         vbox.addWidget(qrw, 1)
1324         vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1325         hbox = QHBoxLayout()
1326         hbox.addStretch(1)
1327
1328         def print_qr(self):
1329             filename = "qrcode.bmp"
1330             bmp.save_qrcode(qrw.qr, filename)
1331             QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1332
1333         b = QPushButton(_("Save"))
1334         hbox.addWidget(b)
1335         b.clicked.connect(print_qr)
1336
1337         b = QPushButton(_("Close"))
1338         hbox.addWidget(b)
1339         b.clicked.connect(d.accept)
1340         b.setDefault(True)
1341
1342         vbox.addLayout(hbox)
1343         d.setLayout(vbox)
1344         d.exec_()
1345
1346
1347     def do_protect(self, func, args):
1348         if self.wallet.use_encryption:
1349             password = self.password_dialog()
1350             if not password:
1351                 return
1352         else:
1353             password = None
1354             
1355         if args != (False,):
1356             args = (self,) + args + (password,)
1357         else:
1358             args = (self,password)
1359         apply( func, args)
1360
1361
1362     @protected
1363     def show_private_key(self, address, password):
1364         if not address: return
1365         try:
1366             pk = self.wallet.get_private_key(address, password)
1367         except BaseException, e:
1368             self.show_message(str(e))
1369             return
1370         QMessageBox.information(self, _('Private key'), 'Address'+ ': ' + address + '\n\n' + _('Private key') + ': ' + pk, _('OK'))
1371
1372
1373     @protected
1374     def do_sign(self, address, message, signature, password):
1375         try:
1376             sig = self.wallet.sign_message(str(address.text()), str(message.toPlainText()), password)
1377             signature.setText(sig)
1378         except BaseException, e:
1379             self.show_message(str(e))
1380
1381     def sign_message(self, address):
1382         if not address: return
1383         d = QDialog(self)
1384         d.setModal(1)
1385         d.setWindowTitle(_('Sign Message'))
1386         d.setMinimumSize(410, 290)
1387
1388         tab_widget = QTabWidget()
1389         tab = QWidget()
1390         layout = QGridLayout(tab)
1391
1392         sign_address = QLineEdit()
1393
1394         sign_address.setText(address)
1395         layout.addWidget(QLabel(_('Address')), 1, 0)
1396         layout.addWidget(sign_address, 1, 1)
1397
1398         sign_message = QTextEdit()
1399         layout.addWidget(QLabel(_('Message')), 2, 0)
1400         layout.addWidget(sign_message, 2, 1)
1401         layout.setRowStretch(2,3)
1402
1403         sign_signature = QTextEdit()
1404         layout.addWidget(QLabel(_('Signature')), 3, 0)
1405         layout.addWidget(sign_signature, 3, 1)
1406         layout.setRowStretch(3,1)
1407
1408
1409         hbox = QHBoxLayout()
1410         b = QPushButton(_("Sign"))
1411         hbox.addWidget(b)
1412         b.clicked.connect(lambda: self.do_sign(sign_address, sign_message, sign_signature))
1413         b = QPushButton(_("Close"))
1414         b.clicked.connect(d.accept)
1415         hbox.addWidget(b)
1416         layout.addLayout(hbox, 4, 1)
1417         tab_widget.addTab(tab, _("Sign"))
1418
1419
1420         tab = QWidget()
1421         layout = QGridLayout(tab)
1422
1423         verify_address = QLineEdit()
1424         layout.addWidget(QLabel(_('Address')), 1, 0)
1425         layout.addWidget(verify_address, 1, 1)
1426
1427         verify_message = QTextEdit()
1428         layout.addWidget(QLabel(_('Message')), 2, 0)
1429         layout.addWidget(verify_message, 2, 1)
1430         layout.setRowStretch(2,3)
1431
1432         verify_signature = QTextEdit()
1433         layout.addWidget(QLabel(_('Signature')), 3, 0)
1434         layout.addWidget(verify_signature, 3, 1)
1435         layout.setRowStretch(3,1)
1436
1437         def do_verify():
1438             try:
1439                 self.wallet.verify_message(verify_address.text(), str(verify_signature.toPlainText()), str(verify_message.toPlainText()))
1440                 self.show_message(_("Signature verified"))
1441             except BaseException, e:
1442                 self.show_message(str(e))
1443                 return
1444
1445         hbox = QHBoxLayout()
1446         b = QPushButton(_("Verify"))
1447         b.clicked.connect(do_verify)
1448         hbox.addWidget(b)
1449         b = QPushButton(_("Close"))
1450         b.clicked.connect(d.accept)
1451         hbox.addWidget(b)
1452         layout.addLayout(hbox, 4, 1)
1453         tab_widget.addTab(tab, _("Verify"))
1454
1455         vbox = QVBoxLayout()
1456         vbox.addWidget(tab_widget)
1457         d.setLayout(vbox)
1458         d.exec_()
1459
1460         
1461
1462
1463     def question(self, msg):
1464         return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1465
1466     def show_message(self, msg):
1467         QMessageBox.information(self, _('Message'), msg, _('OK'))
1468
1469     def password_dialog(self ):
1470         d = QDialog(self)
1471         d.setModal(1)
1472
1473         pw = QLineEdit()
1474         pw.setEchoMode(2)
1475
1476         vbox = QVBoxLayout()
1477         msg = _('Please enter your password')
1478         vbox.addWidget(QLabel(msg))
1479
1480         grid = QGridLayout()
1481         grid.setSpacing(8)
1482         grid.addWidget(QLabel(_('Password')), 1, 0)
1483         grid.addWidget(pw, 1, 1)
1484         vbox.addLayout(grid)
1485
1486         vbox.addLayout(ok_cancel_buttons(d))
1487         d.setLayout(vbox)
1488
1489         self.run_hook('password_dialog', pw, grid, 1)
1490         if not d.exec_(): return
1491         return unicode(pw.text())
1492
1493
1494
1495
1496
1497     @staticmethod
1498     def change_password_dialog( wallet, parent=None ):
1499
1500         if not wallet.seed:
1501             QMessageBox.information(parent, _('Error'), _('No seed'), _('OK'))
1502             return
1503
1504         d = QDialog(parent)
1505         d.setModal(1)
1506
1507         pw = QLineEdit()
1508         pw.setEchoMode(2)
1509         new_pw = QLineEdit()
1510         new_pw.setEchoMode(2)
1511         conf_pw = QLineEdit()
1512         conf_pw.setEchoMode(2)
1513
1514         vbox = QVBoxLayout()
1515         if parent:
1516             msg = (_('Your wallet is encrypted. Use this dialog to change your password.')+'\n'\
1517                    +_('To disable wallet encryption, enter an empty new password.')) \
1518                    if wallet.use_encryption else _('Your wallet keys are not encrypted')
1519         else:
1520             msg = _("Please choose a password to encrypt your wallet keys.")+'\n'\
1521                   +_("Leave these fields empty if you want to disable encryption.")
1522         vbox.addWidget(QLabel(msg))
1523
1524         grid = QGridLayout()
1525         grid.setSpacing(8)
1526
1527         if wallet.use_encryption:
1528             grid.addWidget(QLabel(_('Password')), 1, 0)
1529             grid.addWidget(pw, 1, 1)
1530
1531         grid.addWidget(QLabel(_('New Password')), 2, 0)
1532         grid.addWidget(new_pw, 2, 1)
1533
1534         grid.addWidget(QLabel(_('Confirm Password')), 3, 0)
1535         grid.addWidget(conf_pw, 3, 1)
1536         vbox.addLayout(grid)
1537
1538         vbox.addLayout(ok_cancel_buttons(d))
1539         d.setLayout(vbox) 
1540
1541         if not d.exec_(): return
1542
1543         password = unicode(pw.text()) if wallet.use_encryption else None
1544         new_password = unicode(new_pw.text())
1545         new_password2 = unicode(conf_pw.text())
1546
1547         try:
1548             seed = wallet.decode_seed(password)
1549         except:
1550             QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
1551             return
1552
1553         if new_password != new_password2:
1554             QMessageBox.warning(parent, _('Error'), _('Passwords do not match'), _('OK'))
1555             return ElectrumWindow.change_password_dialog(wallet, parent) # Retry
1556
1557         wallet.update_password(seed, password, new_password)
1558         if parent: 
1559             icon = QIcon(":icons/lock.png") if wallet.use_encryption else QIcon(":icons/unlock.png")
1560             parent.password_button.setIcon( icon )
1561
1562
1563     @staticmethod
1564     def seed_dialog(wallet, parent=None):
1565         d = QDialog(parent)
1566         d.setModal(1)
1567
1568         vbox = QVBoxLayout()
1569         msg = _("Please enter your wallet seed (or your master public key if you want to create a watching-only wallet)." + '\n')
1570         vbox.addWidget(QLabel(msg))
1571
1572         grid = QGridLayout()
1573         grid.setSpacing(8)
1574
1575         seed_e = QLineEdit()
1576         grid.addWidget(QLabel(_('Seed or master public key')), 1, 0)
1577         grid.addWidget(seed_e, 1, 1)
1578         grid.addWidget(HelpButton(_("Your seed can be entered as a mnemonic (sequence of words), or as a hexadecimal string.")), 1, 3)
1579
1580         gap_e = QLineEdit()
1581         gap_e.setText("5")
1582         grid.addWidget(QLabel(_('Gap limit')), 2, 0)
1583         grid.addWidget(gap_e, 2, 1)
1584         grid.addWidget(HelpButton(_('Keep the default value unless you modified this parameter in your wallet.')), 2, 3)
1585         gap_e.textChanged.connect(lambda: numbify(gap_e,True))
1586         vbox.addLayout(grid)
1587
1588         vbox.addLayout(ok_cancel_buttons(d))
1589         d.setLayout(vbox) 
1590
1591         if not d.exec_(): return
1592
1593         try:
1594             gap = int(unicode(gap_e.text()))
1595         except:
1596             QMessageBox.warning(None, _('Error'), 'error', 'OK')
1597             return
1598
1599         try:
1600             seed = str(seed_e.text())
1601             seed.decode('hex')
1602         except:
1603             print_error("Warning: Not hex, trying decode")
1604             try:
1605                 seed = mnemonic.mn_decode( seed.split(' ') )
1606             except:
1607                 QMessageBox.warning(None, _('Error'), _('I cannot decode this'), _('OK'))
1608                 return
1609
1610         if not seed:
1611             QMessageBox.warning(None, _('Error'), _('No seed'), _('OK'))
1612             return
1613
1614         return seed, gap
1615
1616     def generate_transaction_information_widget(self, tx):
1617         tabs = QTabWidget(self)
1618
1619         tab1 = QWidget()
1620         grid_ui = QGridLayout(tab1)
1621         grid_ui.setColumnStretch(0,1)
1622         tabs.addTab(tab1, _('Outputs') )
1623
1624         tree_widget = MyTreeWidget(self)
1625         tree_widget.setColumnCount(2)
1626         tree_widget.setHeaderLabels( [_('Address'), _('Amount')] )
1627         tree_widget.setColumnWidth(0, 300)
1628         tree_widget.setColumnWidth(1, 50)
1629
1630         for address, value in tx.outputs:
1631             item = QTreeWidgetItem( [address, "%s" % ( format_satoshis(value))] )
1632             tree_widget.addTopLevelItem(item)
1633
1634         tree_widget.setMaximumHeight(100)
1635
1636         grid_ui.addWidget(tree_widget)
1637
1638         tab2 = QWidget()
1639         grid_ui = QGridLayout(tab2)
1640         grid_ui.setColumnStretch(0,1)
1641         tabs.addTab(tab2, _('Inputs') )
1642         
1643         tree_widget = MyTreeWidget(self)
1644         tree_widget.setColumnCount(2)
1645         tree_widget.setHeaderLabels( [ _('Address'), _('Previous output')] )
1646
1647         for input_line in tx.inputs:
1648             item = QTreeWidgetItem( [ str(input_line["address"]), str(input_line["prevout_hash"])] )
1649             tree_widget.addTopLevelItem(item)
1650
1651         tree_widget.setMaximumHeight(100)
1652
1653         grid_ui.addWidget(tree_widget)
1654         return tabs
1655
1656
1657     def tx_dict_from_text(self, txt):
1658         try:
1659             tx_dict = json.loads(str(txt))
1660             assert "hex" in tx_dict.keys()
1661             assert "complete" in tx_dict.keys()
1662             if not tx_dict["complete"]:
1663                 assert "input_info" in tx_dict.keys()
1664         except:
1665             QMessageBox.critical(None, "Unable to parse transaction", _("Electrum was unable to parse your transaction"))
1666             return None
1667         return tx_dict
1668
1669
1670     def read_tx_from_file(self):
1671         fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1672         if not fileName:
1673             return
1674         try:
1675             with open(fileName, "r") as f:
1676                 file_content = f.read()
1677         except (ValueError, IOError, os.error), reason:
1678             QMessageBox.critical(None,"Unable to read file or no transaction found", _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1679
1680         return self.tx_dict_from_text(file_content)
1681
1682
1683     @protected
1684     def sign_raw_transaction(self, tx, input_info, dialog ="", password = ""):
1685         try:
1686             self.wallet.signrawtransaction(tx, input_info, [], password)
1687             
1688             fileName = self.getSaveFileName(_("Select where to save your signed transaction"), 'signed_%s.txn' % (tx.hash()[0:8]), "*.txn")
1689             if fileName:
1690                 with open(fileName, "w+") as f:
1691                     f.write(json.dumps(tx.as_dict(),indent=4) + '\n')
1692                 self.show_message(_("Transaction saved successfully"))
1693                 if dialog:
1694                     dialog.done(0)
1695         except BaseException, e:
1696             self.show_message(str(e))
1697     
1698
1699     def send_raw_transaction(self, raw_tx, dialog = ""):
1700         result, result_message = self.wallet.sendtx( raw_tx )
1701         if result:
1702             self.show_message("Transaction successfully sent: %s" % (result_message))
1703             if dialog:
1704                 dialog.done(0)
1705         else:
1706             self.show_message("There was a problem sending your transaction:\n %s" % (result_message))
1707
1708     def do_process_from_text(self):
1709         text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1710         if not text:
1711             return
1712         tx_dict = self.tx_dict_from_text(text)
1713         if tx_dict:
1714             self.create_process_transaction_window(tx_dict)
1715
1716     def do_process_from_file(self):
1717         tx_dict = self.read_tx_from_file()
1718         if tx_dict: 
1719             self.create_process_transaction_window(tx_dict)
1720
1721     def create_process_transaction_window(self, tx_dict):
1722         tx = Transaction(tx_dict["hex"])
1723             
1724         dialog = QDialog(self)
1725         dialog.setMinimumWidth(500)
1726         dialog.setWindowTitle(_('Process raw transaction'))
1727         dialog.setModal(1)
1728
1729         l = QGridLayout()
1730         dialog.setLayout(l)
1731
1732         l.addWidget(QLabel(_("Transaction status:")), 3,0)
1733         l.addWidget(QLabel(_("Actions")), 4,0)
1734
1735         if tx_dict["complete"] == False:
1736             l.addWidget(QLabel(_("Unsigned")), 3,1)
1737             if self.wallet.seed :
1738                 b = QPushButton("Sign transaction")
1739                 input_info = json.loads(tx_dict["input_info"])
1740                 b.clicked.connect(lambda: self.sign_raw_transaction(tx, input_info, dialog))
1741                 l.addWidget(b, 4, 1)
1742             else:
1743                 l.addWidget(QLabel(_("Wallet is de-seeded, can't sign.")), 4,1)
1744         else:
1745             l.addWidget(QLabel(_("Signed")), 3,1)
1746             b = QPushButton("Broadcast transaction")
1747             b.clicked.connect(lambda: self.send_raw_transaction(tx, dialog))
1748             l.addWidget(b,4,1)
1749
1750         l.addWidget( self.generate_transaction_information_widget(tx), 0,0,2,3)
1751         cancelButton = QPushButton(_("Cancel"))
1752         cancelButton.clicked.connect(lambda: dialog.done(0))
1753         l.addWidget(cancelButton, 4,2)
1754
1755         dialog.exec_()
1756
1757
1758     @protected
1759     def do_export_privkeys(self, password):
1760         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.")))
1761
1762         try:
1763             select_export = _('Select file to export your private keys to')
1764             fileName = self.getSaveFileName(select_export, 'electrum-private-keys.csv', "*.csv")
1765             if fileName:
1766                 with open(fileName, "w+") as csvfile:
1767                     transaction = csv.writer(csvfile)
1768                     transaction.writerow(["address", "private_key"])
1769
1770                     
1771                     for addr, pk in self.wallet.get_private_keys(self.wallet.addresses(True), password).items():
1772                         transaction.writerow(["%34s"%addr,pk])
1773
1774                     self.show_message(_("Private keys exported."))
1775
1776         except (IOError, os.error), reason:
1777             export_error_label = _("Electrum was unable to produce a private key-export.")
1778             QMessageBox.critical(None,"Unable to create csv", export_error_label + "\n" + str(reason))
1779
1780         except BaseException, e:
1781           self.show_message(str(e))
1782           return
1783
1784
1785     def do_import_labels(self):
1786         labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
1787         if not labelsFile: return
1788         try:
1789             f = open(labelsFile, 'r')
1790             data = f.read()
1791             f.close()
1792             for key, value in json.loads(data).items():
1793                 self.wallet.labels[key] = value
1794             self.wallet.save()
1795             QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
1796         except (IOError, os.error), reason:
1797             QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
1798             
1799
1800     def do_export_labels(self):
1801         labels = self.wallet.labels
1802         try:
1803             fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
1804             if fileName:
1805                 with open(fileName, 'w+') as f:
1806                     json.dump(labels, f)
1807                 QMessageBox.information(None, "Labels exported", _("Your labels where exported to")+" '%s'" % str(fileName))
1808         except (IOError, os.error), reason:
1809             QMessageBox.critical(None, "Unable to export labels", _("Electrum was unable to export your labels.")+"\n" + str(reason))
1810
1811
1812     def do_export_history(self):
1813         from gui_lite import csv_transaction
1814         csv_transaction(self.wallet)
1815
1816
1817     @protected
1818     def do_import_privkey(self, password):
1819         if not self.wallet.imported_keys:
1820             r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
1821                                          + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
1822                                          + _('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>' \
1823                                          + _('Are you sure you understand what you are doing?'), 3, 4)
1824             if r == 4: return
1825
1826         text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
1827         if not text: return
1828
1829         text = str(text).split()
1830         badkeys = []
1831         addrlist = []
1832         for key in text:
1833             try:
1834                 addr = self.wallet.import_key(key, password)
1835             except BaseException as e:
1836                 badkeys.append(key)
1837                 continue
1838             if not addr: 
1839                 badkeys.append(key)
1840             else:
1841                 addrlist.append(addr)
1842         if addrlist:
1843             QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
1844         if badkeys:
1845             QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
1846         self.update_receive_tab()
1847         self.update_history_tab()
1848
1849
1850     def settings_dialog(self):
1851         d = QDialog(self)
1852         d.setWindowTitle(_('Electrum Settings'))
1853         d.setModal(1)
1854         vbox = QVBoxLayout()
1855
1856         tabs = QTabWidget(self)
1857         self.settings_tab = tabs
1858         vbox.addWidget(tabs)
1859
1860         tab1 = QWidget()
1861         grid_ui = QGridLayout(tab1)
1862         grid_ui.setColumnStretch(0,1)
1863         tabs.addTab(tab1, _('Display') )
1864
1865         nz_label = QLabel(_('Display zeros'))
1866         grid_ui.addWidget(nz_label, 0, 0)
1867         nz_e = QLineEdit()
1868         nz_e.setText("%d"% self.wallet.num_zeros)
1869         grid_ui.addWidget(nz_e, 0, 1)
1870         msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1871         grid_ui.addWidget(HelpButton(msg), 0, 2)
1872         nz_e.textChanged.connect(lambda: numbify(nz_e,True))
1873         if not self.config.is_modifiable('num_zeros'):
1874             for w in [nz_e, nz_label]: w.setEnabled(False)
1875         
1876         lang_label=QLabel(_('Language') + ':')
1877         grid_ui.addWidget(lang_label, 1, 0)
1878         lang_combo = QComboBox()
1879         from i18n import languages
1880         lang_combo.addItems(languages.values())
1881         try:
1882             index = languages.keys().index(self.config.get("language",''))
1883         except:
1884             index = 0
1885         lang_combo.setCurrentIndex(index)
1886         grid_ui.addWidget(lang_combo, 1, 1)
1887         grid_ui.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
1888         if not self.config.is_modifiable('language'):
1889             for w in [lang_combo, lang_label]: w.setEnabled(False)
1890
1891         currencies = self.exchanger.get_currencies()
1892         currencies.insert(0, "None")
1893
1894         cur_label=QLabel(_('Currency') + ':')
1895         grid_ui.addWidget(cur_label , 2, 0)
1896         cur_combo = QComboBox()
1897         cur_combo.addItems(currencies)
1898         try:
1899             index = currencies.index(self.config.get('currency', "None"))
1900         except:
1901             index = 0
1902         cur_combo.setCurrentIndex(index)
1903         grid_ui.addWidget(cur_combo, 2, 1)
1904         grid_ui.addWidget(HelpButton(_('Select which currency is used for quotes.')+' '), 2, 2)
1905         
1906         view_label=QLabel(_('Receive Tab') + ':')
1907         grid_ui.addWidget(view_label , 3, 0)
1908         view_combo = QComboBox()
1909         view_combo.addItems([_('Simple'), _('Advanced')])
1910         view_combo.setCurrentIndex(self.expert_mode)
1911         grid_ui.addWidget(view_combo, 3, 1)
1912         hh = _('This selects the interaction mode of the "Receive" tab.')+' ' + '\n\n' \
1913              + _('Simple') +   ': ' + _('Show only addresses and labels.') + '\n\n' \
1914              + _('Advanced') + ': ' + _('Show address balances and add extra menu items to freeze/prioritize addresses.') + '\n\n' 
1915         
1916         grid_ui.addWidget(HelpButton(hh), 3, 2)
1917         grid_ui.setRowStretch(4,1)
1918
1919         # wallet tab
1920         tab2 = QWidget()
1921         grid_wallet = QGridLayout(tab2)
1922         grid_wallet.setColumnStretch(0,1)
1923         tabs.addTab(tab2, _('Wallet') )
1924         
1925         fee_label = QLabel(_('Transaction fee'))
1926         grid_wallet.addWidget(fee_label, 0, 0)
1927         fee_e = QLineEdit()
1928         fee_e.setText("%s"% str( Decimal( self.wallet.fee)/100000000 ) )
1929         grid_wallet.addWidget(fee_e, 0, 2)
1930         msg = _('Fee per transaction input. Transactions involving multiple inputs tend to require a higher fee.') + ' ' \
1931             + _('Recommended value') + ': 0.001'
1932         grid_wallet.addWidget(HelpButton(msg), 0, 3)
1933         fee_e.textChanged.connect(lambda: numbify(fee_e,False))
1934         if not self.config.is_modifiable('fee'):
1935             for w in [fee_e, fee_label]: w.setEnabled(False)
1936
1937         usechange_label = QLabel(_('Use change addresses'))
1938         grid_wallet.addWidget(usechange_label, 1, 0)
1939         usechange_combo = QComboBox()
1940         usechange_combo.addItems([_('Yes'), _('No')])
1941         usechange_combo.setCurrentIndex(not self.wallet.use_change)
1942         grid_wallet.addWidget(usechange_combo, 1, 2)
1943         grid_wallet.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 1, 3)
1944         if not self.config.is_modifiable('use_change'): usechange_combo.setEnabled(False)
1945
1946         gap_label = QLabel(_('Gap limit'))
1947         grid_wallet.addWidget(gap_label, 2, 0)
1948         gap_e = QLineEdit()
1949         gap_e.setText("%d"% self.wallet.gap_limit)
1950         grid_wallet.addWidget(gap_e, 2, 2)
1951         msg =  _('The gap limit is the maximal number of contiguous unused addresses in your sequence of receiving addresses.') + '\n' \
1952               + _('You may increase it if you need more receiving addresses.') + '\n\n' \
1953               + _('Your current gap limit is') + ': %d'%self.wallet.gap_limit + '\n' \
1954               + _('Given the current status of your address sequence, the minimum gap limit you can use is:')+' ' + '%d'%self.wallet.min_acceptable_gap() + '\n\n' \
1955               + _('Warning') + ': ' \
1956               + _('The gap limit parameter must be provided in order to recover your wallet from seed.') + ' ' \
1957               + _('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' 
1958         grid_wallet.addWidget(HelpButton(msg), 2, 3)
1959         gap_e.textChanged.connect(lambda: numbify(nz_e,True))
1960         if not self.config.is_modifiable('gap_limit'):
1961             for w in [gap_e, gap_label]: w.setEnabled(False)
1962
1963         grid_wallet.setRowStretch(3,1)
1964
1965
1966         # import/export tab
1967         tab3 = QWidget()
1968         grid_io = QGridLayout(tab3)
1969         grid_io.setColumnStretch(0,1)
1970         tabs.addTab(tab3, _('Import/Export') )
1971         
1972         grid_io.addWidget(QLabel(_('Labels')), 1, 0)
1973         grid_io.addWidget(EnterButton(_("Export"), self.do_export_labels), 1, 1)
1974         grid_io.addWidget(EnterButton(_("Import"), self.do_import_labels), 1, 2)
1975         grid_io.addWidget(HelpButton(_('Export your labels as json')), 1, 3)
1976
1977         grid_io.addWidget(QLabel(_('History')), 2, 0)
1978         grid_io.addWidget(EnterButton(_("Export"), self.do_export_history), 2, 1)
1979         grid_io.addWidget(HelpButton(_('Export your transaction history as csv')), 2, 3)
1980
1981         grid_io.addWidget(QLabel(_('Private keys')), 3, 0)
1982
1983         grid_io.addWidget(EnterButton(_("Export"), self.do_export_privkeys), 3, 1)
1984         grid_io.addWidget(EnterButton(_("Import"), self.do_import_privkey), 3, 2)
1985         grid_io.addWidget(HelpButton(_('Import private key')), 3, 3)
1986
1987         grid_io.addWidget(QLabel(_('Master Public Key')), 4, 0)
1988         grid_io.addWidget(EnterButton(_("Show"), self.show_master_public_key), 4, 1)
1989         grid_io.addWidget(HelpButton(_('Your Master Public Key can be used to create receiving addresses, but not to sign transactions.') + ' ' \
1990                               + _('If you give it to someone, they will be able to see your transactions, but not to spend your money.') + ' ' \
1991                               + _('If you restore your wallet from it, a watching-only (deseeded) wallet will be created.')), 4, 3)
1992
1993
1994         grid_io.addWidget(QLabel(_("Load transaction")), 5, 0)
1995         grid_io.addWidget(EnterButton(_("From file"), self.do_process_from_file), 5, 1)
1996         grid_io.addWidget(EnterButton(_("From text"), self.do_process_from_text), 5, 2)
1997         grid_io.addWidget(HelpButton(_("This will give you the option to sign or broadcast a transaction based on it's status.")), 5, 3)
1998
1999         grid_io.setRowStretch(5,1)
2000
2001
2002         # plugins
2003         if self.plugins:
2004             tab5 = QScrollArea()
2005             grid_plugins = QGridLayout(tab5)
2006             grid_plugins.setColumnStretch(0,1)
2007             tabs.addTab(tab5, _('Plugins') )
2008             def mk_toggle(cb, p):
2009                 return lambda: cb.setChecked(p.toggle())
2010             for i, p in enumerate(self.plugins):
2011                 try:
2012                     name, description = p.get_info()
2013                     cb = QCheckBox(name)
2014                     cb.setDisabled(not p.is_available())
2015                     cb.setChecked(p.is_enabled())
2016                     cb.clicked.connect(mk_toggle(cb,p))
2017                     grid_plugins.addWidget(cb, i, 0)
2018                     grid_plugins.addWidget(HelpButton(description), i, 1)
2019                 except:
2020                     print_msg("Error: cannot display plugin", p)
2021                     traceback.print_exc(file=sys.stdout)
2022             grid_plugins.setRowStretch(i+1,1)
2023
2024         self.run_hook('create_settings_tab', tabs)
2025
2026         vbox.addLayout(ok_cancel_buttons(d))
2027         d.setLayout(vbox) 
2028
2029         # run the dialog
2030         if not d.exec_(): return
2031
2032         fee = unicode(fee_e.text())
2033         try:
2034             fee = int( 100000000 * Decimal(fee) )
2035         except:
2036             QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2037             return
2038
2039         if self.wallet.fee != fee:
2040             self.wallet.fee = fee
2041             self.wallet.save()
2042         
2043         nz = unicode(nz_e.text())
2044         try:
2045             nz = int( nz )
2046             if nz>8: nz=8
2047         except:
2048             QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2049             return
2050
2051         if self.wallet.num_zeros != nz:
2052             self.wallet.num_zeros = nz
2053             self.config.set_key('num_zeros', nz, True)
2054             self.update_history_tab()
2055             self.update_receive_tab()
2056
2057         usechange_result = usechange_combo.currentIndex() == 0
2058         if self.wallet.use_change != usechange_result:
2059             self.wallet.use_change = usechange_result
2060             self.config.set_key('use_change', self.wallet.use_change, True)
2061         
2062         try:
2063             n = int(gap_e.text())
2064         except:
2065             QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
2066             return
2067
2068         if self.wallet.gap_limit != n:
2069             r = self.wallet.change_gap_limit(n)
2070             if r:
2071                 self.update_receive_tab()
2072                 self.config.set_key('gap_limit', self.wallet.gap_limit, True)
2073             else:
2074                 QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
2075
2076         need_restart = False
2077
2078         lang_request = languages.keys()[lang_combo.currentIndex()]
2079         if lang_request != self.config.get('language'):
2080             self.config.set_key("language", lang_request, True)
2081             need_restart = True
2082             
2083         cur_request = str(currencies[cur_combo.currentIndex()])
2084         if cur_request != self.config.get('currency', "None"):
2085             self.config.set_key('currency', cur_request, True)
2086             self.update_wallet()
2087
2088         self.run_hook('close_settings_dialog')
2089
2090         if need_restart:
2091             QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2092
2093         self.receive_tab_set_mode(view_combo.currentIndex())
2094
2095
2096     @staticmethod 
2097     def network_dialog(wallet, parent=None):
2098         interface = wallet.interface
2099         if parent:
2100             if interface.is_connected:
2101                 status = _("Connected to")+" %s\n%d "%(interface.host, wallet.verifier.height)+_("blocks")
2102             else:
2103                 status = _("Not connected")
2104             server = interface.server
2105         else:
2106             import random
2107             status = _("Please choose a server.") + "\n" + _("Select 'Cancel' if you are offline.")
2108             server = interface.server
2109
2110         plist, servers_list = interface.get_servers_list()
2111
2112         d = QDialog(parent)
2113         d.setModal(1)
2114         d.setWindowTitle(_('Server'))
2115         d.setMinimumSize(375, 20)
2116
2117         vbox = QVBoxLayout()
2118         vbox.setSpacing(30)
2119
2120         hbox = QHBoxLayout()
2121         l = QLabel()
2122         l.setPixmap(QPixmap(":icons/network.png"))
2123         hbox.addStretch(10)
2124         hbox.addWidget(l)
2125         hbox.addWidget(QLabel(status))
2126         hbox.addStretch(50)
2127         vbox.addLayout(hbox)
2128
2129
2130         # grid layout
2131         grid = QGridLayout()
2132         grid.setSpacing(8)
2133         vbox.addLayout(grid)
2134
2135         # server
2136         server_protocol = QComboBox()
2137         server_host = QLineEdit()
2138         server_host.setFixedWidth(200)
2139         server_port = QLineEdit()
2140         server_port.setFixedWidth(60)
2141
2142         protocol_names = ['TCP', 'HTTP', 'TCP/SSL', 'HTTPS']
2143         protocol_letters = 'thsg'
2144         DEFAULT_PORTS = {'t':'50001', 's':'50002', 'h':'8081', 'g':'8082'}
2145         server_protocol.addItems(protocol_names)
2146
2147         grid.addWidget(QLabel(_('Server') + ':'), 0, 0)
2148         grid.addWidget(server_protocol, 0, 1)
2149         grid.addWidget(server_host, 0, 2)
2150         grid.addWidget(server_port, 0, 3)
2151
2152         def change_protocol(p):
2153             protocol = protocol_letters[p]
2154             host = unicode(server_host.text())
2155             pp = plist.get(host,DEFAULT_PORTS)
2156             if protocol not in pp.keys():
2157                 protocol = pp.keys()[0]
2158             port = pp[protocol]
2159             server_host.setText( host )
2160             server_port.setText( port )
2161
2162         server_protocol.connect(server_protocol, SIGNAL('currentIndexChanged(int)'), change_protocol)
2163         
2164         label = _('Active Servers') if wallet.interface.servers else _('Default Servers')
2165         servers_list_widget = QTreeWidget(parent)
2166         servers_list_widget.setHeaderLabels( [ label, _('Type') ] )
2167         servers_list_widget.setMaximumHeight(150)
2168         servers_list_widget.setColumnWidth(0, 240)
2169         for _host in servers_list.keys():
2170             _type = 'P' if servers_list[_host].get('pruning') else 'F'
2171             servers_list_widget.addTopLevelItem(QTreeWidgetItem( [ _host, _type ] ))
2172
2173         def change_server(host, protocol=None):
2174             pp = plist.get(host,DEFAULT_PORTS)
2175             if protocol:
2176                 port = pp.get(protocol)
2177                 if not port: protocol = None
2178                     
2179             if not protocol:
2180                 if 't' in pp.keys():
2181                     protocol = 't'
2182                     port = pp.get(protocol)
2183                 else:
2184                     protocol = pp.keys()[0]
2185                     port = pp.get(protocol)
2186             
2187             server_host.setText( host )
2188             server_port.setText( port )
2189             server_protocol.setCurrentIndex(protocol_letters.index(protocol))
2190
2191             if not plist: return
2192             for p in protocol_letters:
2193                 i = protocol_letters.index(p)
2194                 j = server_protocol.model().index(i,0)
2195                 if p not in pp.keys() and interface.is_connected:
2196                     server_protocol.model().setData(j, QtCore.QVariant(0), QtCore.Qt.UserRole-1)
2197                 else:
2198                     server_protocol.model().setData(j, QtCore.QVariant(0,False), QtCore.Qt.UserRole-1)
2199
2200
2201         if server:
2202             host, port, protocol = server.split(':')
2203             change_server(host,protocol)
2204
2205         servers_list_widget.connect(servers_list_widget, SIGNAL('itemClicked(QTreeWidgetItem*, int)'), lambda x: change_server(unicode(x.text(0))))
2206         grid.addWidget(servers_list_widget, 1, 1, 1, 3)
2207
2208         if not wallet.config.is_modifiable('server'):
2209             for w in [server_host, server_port, server_protocol, servers_list_widget]: w.setEnabled(False)
2210
2211         # auto cycle
2212         autocycle_cb = QCheckBox(_('Try random servers if disconnected'))
2213         autocycle_cb.setChecked(wallet.config.get('auto_cycle', False))
2214         grid.addWidget(autocycle_cb, 3, 1, 3, 2)
2215         if not wallet.config.is_modifiable('auto_cycle'): autocycle_cb.setEnabled(False)
2216
2217         # proxy setting
2218         proxy_mode = QComboBox()
2219         proxy_host = QLineEdit()
2220         proxy_host.setFixedWidth(200)
2221         proxy_port = QLineEdit()
2222         proxy_port.setFixedWidth(60)
2223         proxy_mode.addItems(['NONE', 'SOCKS4', 'SOCKS5', 'HTTP'])
2224
2225         def check_for_disable(index = False):
2226             if proxy_mode.currentText() != 'NONE':
2227                 proxy_host.setEnabled(True)
2228                 proxy_port.setEnabled(True)
2229             else:
2230                 proxy_host.setEnabled(False)
2231                 proxy_port.setEnabled(False)
2232
2233         check_for_disable()
2234         proxy_mode.connect(proxy_mode, SIGNAL('currentIndexChanged(int)'), check_for_disable)
2235
2236         if not wallet.config.is_modifiable('proxy'):
2237             for w in [proxy_host, proxy_port, proxy_mode]: w.setEnabled(False)
2238
2239         proxy_config = interface.proxy if interface.proxy else { "mode":"none", "host":"localhost", "port":"8080"}
2240         proxy_mode.setCurrentIndex(proxy_mode.findText(str(proxy_config.get("mode").upper())))
2241         proxy_host.setText(proxy_config.get("host"))
2242         proxy_port.setText(proxy_config.get("port"))
2243
2244         grid.addWidget(QLabel(_('Proxy') + ':'), 2, 0)
2245         grid.addWidget(proxy_mode, 2, 1)
2246         grid.addWidget(proxy_host, 2, 2)
2247         grid.addWidget(proxy_port, 2, 3)
2248
2249         # buttons
2250         vbox.addLayout(ok_cancel_buttons(d))
2251         d.setLayout(vbox) 
2252
2253         if not d.exec_(): return
2254
2255         server = unicode( server_host.text() ) + ':' + unicode( server_port.text() ) + ':' + (protocol_letters[server_protocol.currentIndex()])
2256         if proxy_mode.currentText() != 'NONE':
2257             proxy = { u'mode':unicode(proxy_mode.currentText()).lower(), u'host':unicode(proxy_host.text()), u'port':unicode(proxy_port.text()) }
2258         else:
2259             proxy = None
2260
2261         wallet.config.set_key("proxy", proxy, True)
2262         wallet.config.set_key("server", server, True)
2263         interface.set_server(server, proxy)
2264         wallet.config.set_key('auto_cycle', autocycle_cb.isChecked(), True)
2265         return True
2266
2267     def closeEvent(self, event):
2268         g = self.geometry()
2269         self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
2270         self.save_column_widths()
2271         self.config.set_key("column-widths", self.column_widths, True)
2272         self.config.set_key("console-history",self.console.history[-50:])
2273         event.accept()
2274
2275
2276 class ElectrumGui:
2277
2278     def __init__(self, wallet, config, app=None):
2279         self.wallet = wallet
2280         self.config = config
2281         if app is None:
2282             self.app = QApplication(sys.argv)
2283
2284
2285     def restore_or_create(self):
2286         msg = _("Wallet file not found.")+"\n"+_("Do you want to create a new wallet, or to restore an existing one?")
2287         r = QMessageBox.question(None, _('Message'), msg, _('Create'), _('Restore'), _('Cancel'), 0, 2)
2288         if r==2: return None
2289         return 'restore' if r==1 else 'create'
2290
2291     def seed_dialog(self):
2292         return ElectrumWindow.seed_dialog( self.wallet )
2293
2294     def network_dialog(self):
2295         return ElectrumWindow.network_dialog( self.wallet, parent=None )
2296         
2297
2298     def show_seed(self):
2299         ElectrumWindow.show_seed(self.wallet.seed)
2300
2301
2302     def password_dialog(self):
2303         if self.wallet.seed:
2304             ElectrumWindow.change_password_dialog(self.wallet)
2305
2306
2307     def restore_wallet(self):
2308         wallet = self.wallet
2309         # wait until we are connected, because the user might have selected another server
2310         if not wallet.interface.is_connected:
2311             waiting = lambda: False if wallet.interface.is_connected else "%s \n" % (_("Connecting..."))
2312             waiting_dialog(waiting)
2313
2314         waiting = lambda: False if wallet.is_up_to_date() else "%s\n%s %d\n%s %.1f"\
2315             %(_("Please wait..."),_("Addresses generated:"),len(wallet.addresses(True)),_("Kilobytes received:"), wallet.interface.bytes_received/1024.)
2316
2317         wallet.set_up_to_date(False)
2318         wallet.interface.poke('synchronizer')
2319         waiting_dialog(waiting)
2320         if wallet.is_found():
2321             print_error( "Recovery successful" )
2322         else:
2323             QMessageBox.information(None, _('Error'), _("No transactions found for this seed"), _('OK'))
2324
2325         return True
2326
2327     def main(self,url):
2328         s = Timer()
2329         s.start()
2330         w = ElectrumWindow(self.wallet, self.config)
2331         if url: w.set_url(url)
2332         w.app = self.app
2333         w.connect_slots(s)
2334         w.update_wallet()
2335         w.show()
2336
2337         self.app.exec_()
2338
2339