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