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