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