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