Bugfix: Always be sure to return a str/unicode from humanAmount
[bitcoin:spesmilo.git] / settings.py
1 # -*- coding: utf-8 -*-
2 # Spesmilo -- Python Bitcoin user interface
3 # Copyright © 2011 Luke Dashjr
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, version 3 only.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
17 from decimal import Decimal
18 import os
19 import re
20 import subprocess
21 from PySide.QtCore import *
22 from PySide.QtGui import *
23
24 import anynumber
25
26 _settings = QSettings('Bitcoin', 'Spesmilo')
27
28 def icon(*ss):
29     if not ss:
30         if not hasattr(icon, '_default'):
31             icon._default = icon(*icon._defaultSearch)
32         return icon._default
33     if len(ss) == 1:
34         ss += ('icons/%s.png' % (ss[0],),)
35     for s in ss:
36         if not s:
37             continue
38         if '/' in s:
39             return QIcon(s)
40         if QIcon.hasThemeIcon(s):
41             return QIcon.fromTheme(s)
42     return QIcon()
43 icon._defaultSearch = ('spesmilo', 'bitcoin', 'icons/bitcoin32.png')
44
45 def quietPopen(*args, **kwargs):
46     if 'startupinfo' not in kwargs and hasattr(subprocess, 'STARTUPINFO'):
47         kwargs['startupinfo'] = subprocess.STARTUPINFO()
48     if 'startupinfo' in kwargs:
49         kwargs['startupinfo'].dwFlags |= subprocess.STARTF_USESHOWWINDOW if hasattr(subprocess, 'STARTF_USESHOWWINDOW') else 1
50     try:
51         return subprocess.Popen(*args, **kwargs)
52     except:
53         class dummy:
54             def wait(self):
55                 return -1
56         return dummy()
57
58 def style_item(item, style, want_old = False):
59     old = None
60     brush = item.foreground()
61     font = item.font()
62     if want_old:
63         old = (brush.color(), font.style())
64     brush.setColor(style[0])
65     font.setStyle(style[1])
66     item.setForeground(brush)
67     item.setFont(font)
68     return old
69
70 disabled_style = (Qt.gray, QFont.StyleItalic)
71 def disable_item(item, *args, **kwargs):
72     return style_item(item, disabled_style, *args, **kwargs)
73
74 def TonalSupported():
75     from PySide.QtGui import qApp
76     if not qApp:
77         return True
78     return QFontMetrics(qApp.font()).inFont(0xe9d9)
79
80 class SettingsTabBASE(QWidget):
81     def __init__(self, parent = None, dlg = None):
82         super(SettingsTabBASE, self).__init__(parent)
83
84         if parent:
85             self.pp = parent.parent()
86             self.pphas = lambda x: hasattr(self.pp, x)
87         else:
88             self.pphas = lambda x: False
89
90         self.options = []
91         self._build()
92
93         self._dlg = dlg
94         nea = lambda: dlg.enableApply()
95         for o in self.options:
96             o._onChange(nea)
97
98     def loadSettings(self, settings = None):
99         for o in self.options:
100             o._CV = o._load(settings)
101             o._OV = o._CV
102             o._set(o._CV)
103
104     def checkSettings(self):
105         pass
106
107     def saveSettings(self, settings = None):
108         RR = False
109         for o in self.options:
110             nv = o._get()
111             if nv != o._CV and nv != o._OV:
112                 o._CV = nv
113                 if hasattr(o, '_apply'):
114                     if o._apply(nv):
115                         RR = True
116                     else:
117                         o._OV = nv
118                 else:
119                     RR = True
120             o._save(settings, nv)
121         if RR:
122             self._dlg.requireRestart()
123
124 class SettingsWidgetMixIn(object):
125     def __init__(self, *args, **kwargs):
126         self._key = kwargs['key']
127         self._default = kwargs['default']
128         del kwargs['key']
129         del kwargs['default']
130         super(SettingsWidgetMixIn, self).__init__(*args, **kwargs)
131
132     def _load(self, settings):
133         return settings.value(self._key, self._default)
134     def _save(self, settings, newvalue):
135         settings.setValue(self._key, newvalue)
136
137 class SettingsQCheckBox(SettingsWidgetMixIn, QCheckBox):
138     def _onChange(self, slot):
139         self.stateChanged.connect(slot)
140
141     def _load(self, settings):
142         r = settings.value(self._key, None)
143         if r is None:
144             return self._default
145         return r != 'False'
146     def _save(self, settings, newvalue):
147         settings.setValue(self._key, repr(newvalue))
148
149     def _get(self):
150         return self.isChecked()
151     def _set(self, newvalue):
152         self.setChecked(newvalue)
153
154 class SettingsQComboBox(SettingsWidgetMixIn, QComboBox):
155     def _onChange(self, slot):
156         self.currentIndexChanged.connect(slot)
157
158     def _get(self):
159         return self.itemData(self.currentIndex())
160     def _set(self, newvalue):
161         self.setCurrentIndex(self.findData(newvalue))
162
163 class SettingsQLineEdit(SettingsWidgetMixIn, QLineEdit):
164     def _onChange(self, slot):
165         self.textChanged.connect(slot)
166
167     def _get(self):
168         return self.text()
169     def _set(self, newvalue):
170         self.setText(newvalue)
171
172 class SettingsTabCore(SettingsTabBASE):
173     def _build(self):
174         cblay = QFormLayout()
175         self.cbInternal = SettingsQCheckBox(self, key='core/internal', default=True)
176         self.options.append(self.cbInternal)
177         self.lblInternal = QLabel(self.tr('Use internal core'))
178         cblay.addRow(self.cbInternal, self.lblInternal)
179         self.cbInternal.stateChanged.connect(self.updateURIValidity)
180         
181         lelay = QFormLayout()
182         self.lblURI = QLabel(self.tr('URI:'))
183         self.leURI = SettingsQLineEdit(key='core/uri', default='http://user:pass@localhost:8332')
184         self.options.append(self.leURI)
185         lelay.addRow(self.lblURI, self.leURI)
186
187         mainlay = QVBoxLayout(self)
188         mainlay.addLayout(cblay)
189         mainlay.addLayout(lelay)
190     
191     def checkSettings(self):
192         if quietPopen( ('bitcoind', '--help') ).wait():
193             self.cbInternal.setChecked(False)
194             self.cbInternal.setEnabled(False)
195             self.lblInternal.setEnabled(False)
196     
197     def updateURIValidity(self):
198         en = not self.cbInternal.isChecked()
199         self.lblURI.setEnabled(en)
200         self.leURI.setEnabled(en)
201
202 class SettingsTabLanguage(SettingsTabBASE):
203     def _build(self):
204         self._deferred = {}
205         
206         mainlay = QFormLayout(self)
207         
208         self.lang = SettingsQComboBox(key='language/language', default='')
209         langM = QStandardItemModel()
210         self.lang.setModel(langM)
211         langlist = [
212             (self.tr('(Default)'), ''),
213             (self.tr('American'), 'en_US'),
214             (self.tr('English'), 'en_GB'),
215             (self.tr('Esperanto'), 'eo'),
216             (self.tr('Dutch'), 'nl'),
217             (self.tr('French'), 'fr'),
218         ]
219         langlist.sort()
220         for i in xrange(len(langlist)):
221             lang = langlist[i]
222             self.lang.addItem(*lang)
223             if not (lang[1] in ('', 'en_US') or os.path.exists("i18n/%s.qm" % (lang[1],))):
224                 item = langM.item(i, 0)
225                 item.setSelectable(False)
226                 disable_item(item)
227         self.options.append(self.lang)
228         mainlay.addRow(self.tr('Language:'), self.lang)
229         
230         nslay = QHBoxLayout()
231         self.strength = SettingsQComboBox(key='units/strength', default='Assume')
232         self.strength._apply = lambda nv: self._defer_update('amounts')
233         self.strength.addItem(self.tr('Assume'), 'Assume')
234         self.strength.addItem(self.tr('Prefer'), 'Prefer')
235         self.strength.addItem(self.tr('Force'), 'Force')
236         self.options.append(self.strength)
237         nslay.addWidget(self.strength)
238         self.numsys = SettingsQComboBox(self, key='units/numsys', default='Decimal')
239         self.numsys._apply = lambda nv: self._defer_update('counters', 'amounts')
240         numsysM = QStandardItemModel()
241         self.numsys.setModel(numsysM)
242         self.numsys.addItem(self.tr('Decimal'), 'Decimal')
243         self.numsys.addItem(self.tr('Tonal'), 'Tonal')
244         if not TonalSupported():
245             item = numsysM.item(1, 0)
246             item.setSelectable(False)
247             disable_item(item)
248         self.options.append(self.numsys)
249         nslay.addWidget(self.numsys)
250         mainlay.addRow(self.tr('Number system:'), nslay)
251         
252         self.hideTLA = SettingsQCheckBox(self.tr('Hide preferred unit name'), key='units/hideTLA', default=True)
253         self.hideTLA._apply = lambda nv: self._defer_update('amounts')
254         self.options.append(self.hideTLA)
255         mainlay.addRow(self.hideTLA)
256     
257     def _defer_update(self, *updates):
258         RR = False
259         for update in updates:
260             update = 'update_%s' % (update,)
261             if self.pphas(update):
262                 self._deferred[update] = True
263             else:
264                 RR = True
265         return RR
266
267     def saveSettings(self, *args, **kwargs):
268         super(SettingsTabLanguage, self).saveSettings(*args, **kwargs)
269         for du in self._deferred.iterkeys():
270             getattr(self.pp, du)()
271         self._deferred = {}
272
273 class SettingsDialog(QDialog):
274     def __init__(self, parent):
275         super(SettingsDialog, self).__init__(parent)
276         
277         self.settings = _settings
278         
279         tabw = QTabWidget()
280         
281         self.tabs = []
282         self.tabs.append((self.tr('Core'), SettingsTabCore(self, self)))
283         self.tabs.append((self.tr('Language'), SettingsTabLanguage(self, self)))
284         
285         for name, widget in self.tabs:
286             tabw.addTab(widget, name)
287         
288         mainlay = QVBoxLayout(self)
289         mainlay.addWidget(tabw)
290         
291         actionlay = QHBoxLayout()
292         actionlay.addStretch()
293         
294         okbtn = QPushButton(self.tr('&OK'))
295         okbtn.clicked.connect(self.accept)
296         okbtn.setAutoDefault(True)
297         actionlay.addWidget(okbtn)
298         
299         applybtn = QPushButton(self.tr('&Apply'))
300         self.applybtn = applybtn
301         applybtn.clicked.connect(lambda: self.saveSettings())
302         actionlay.addWidget(applybtn)
303         
304         cancelbtn = QPushButton(self.tr('&Cancel'))
305         cancelbtn.clicked.connect(self.reject)
306         actionlay.addWidget(cancelbtn)
307
308         mainlay.addLayout(actionlay)
309         
310         self.loadSettings()
311         self.checkSettings()
312         
313         self.accepted.connect(lambda: self.saveSettings())
314         
315         self.setWindowIcon(icon())
316         self.setWindowTitle(self.tr('Settings'))
317         self.show()
318     
319     def loadSettings(self):
320         settings = self.settings
321         for x, widget in self.tabs:
322             widget.loadSettings(settings)
323         self.applybtn.setEnabled(False)
324     
325     def checkSettings(self):
326         for x, widget in self.tabs:
327             widget.checkSettings()
328         
329     def saveSettings(self):
330         settings = self.settings
331         for x, widget in self.tabs:
332             widget.saveSettings(settings)
333         self.applybtn.setEnabled(False)
334
335     def enableApply(self):
336         self.applybtn.setEnabled(True)
337
338     def requireRestart(self):
339         if not (hasattr(self.parent(), 'core') and self.parent().core):
340             return
341         msg = QMessageBox(QMessageBox.Information,
342                           self.tr('Restart required'),
343                           self.tr('Restarting Spesmilo is required for some changes to take effect.'))
344         msg.exec_()
345
346 class SpesmiloSettings:
347     def isConfigured(self):
348         NC = 'NOT CONFIGURED'
349         return _settings.value('core/internal', NC) != NC
350     
351     def useInternalCore(self):
352         return _settings.value('core/internal', 'True') != 'False'
353     
354     def getInternalCoreAuth(self):
355         if not hasattr(self, '_ICA'):
356             from random import random
357             passwd = random()
358             self._ICA = ('spesmilo', passwd, 8342)
359         return self._ICA
360
361     def getEffectiveURI(self):
362         if self.useInternalCore():
363             return 'http://%s:%s@127.0.0.1:%d' % self.getInternalCoreAuth()
364         return _settings.value('core/uri', 'http://user:pass@localhost:8332')
365
366     def getNumberSystem(self):
367         if not TonalSupported():
368             return 'Decimal'
369         return _settings.value('units/numsys', 'Decimal')
370
371     def getNumberSystemStrength(self):
372         return _settings.value('units/strength', 'Assume')
373
374     def getHideUnitTLA(self):
375         return _settings.value('units/hideTLA', 'True') != 'False'
376
377     def format_number(self, n, addSign = False, wantDelimiters = False):
378         ns = self.getNumberSystem()
379         if ns == 'Tonal':
380             n = anynumber.Tonal(n)
381         else:
382             n = anynumber.Decimal(n)
383         return n.format(addSign=addSign, wantDelimiters=wantDelimiters)
384
385     def _toBTC(self, n, addSign = False, wantTLA = None, wantDelimiters = False):
386         n = anynumber.Decimal(n) / 100000000
387         s = n.format(addSign=addSign, wantDelimiters=wantDelimiters)
388         if '.' not in s:
389             s += '.00'
390         elif s[-2] == '.':
391             s += '0'
392         if wantTLA is None:
393             wantTLA = not self.getHideUnitTLA()
394         if wantTLA:
395             s += " BTC"
396         return s
397
398     def _fromBTC(self, s):
399         s = anynumber.Decimal(s)
400         s = int(s * 100000000)
401         return s
402
403     def _toTBC(self, n, addSign = False, wantTLA = None, wantDelimiters = False):
404         n = anynumber.Tonal(n) / 0x10000
405         s = n.format(addSign=addSign, wantDelimiters=wantDelimiters)
406         if wantTLA is None:
407             wantTLA = not self.getHideUnitTLA()
408         if wantTLA:
409             s += " TBC"
410         return s
411
412     def _fromTBC(self, s):
413         n = int(anynumber.Tonal(s) * 0x10000)
414         return n
415
416     def ChooseUnits(self, n, guess = None):
417         if float(n) != n:
418             raise ValueError()
419         ns = self.getNumberSystem()
420         nss = self.getNumberSystemStrength()
421         ens = None
422         if nss != 'Force' and n and TonalSupported():
423             # If it's only valid as one, and not the other, choose it
424             ivD = 0 == n % 1000000
425             ivT = 0 == n % 0x100
426             if ivD and not ivT:
427                 ens = 'Decimal'
428             elif ivT and not ivD:
429                 ens = 'Tonal'
430             # If it could be either, pick the more likely one (only with 'Assume')
431             elif ivD and nss == 'Assume':
432                 if not guess is None:
433                     ens = guess
434                 dn = n / 1000000
435                 tn = n / 0x100
436                 while ens is None:
437                     dm = dn % 10 not in (0, 5)
438                     tm = tn % 0x10 not in (0, 8)
439                     if dm:
440                         if tm:
441                             break
442                         ens = 'Tonal'
443                     elif tm:
444                         ens = 'Decimal'
445                     dn /= 10.
446                     tn /= 16.
447         if ens is None: ens = ns
448         return ens
449
450     def humanAmount(self, n, addSign = False, wantTLA = None):
451         ns = self.getNumberSystem()
452         try:
453             ens = self.ChooseUnits(n)
454         except ValueError:
455             return str(n)
456         if ens != ns:
457             wantTLA = True
458         if ens == 'Tonal':
459             ens = self._toTBC
460         else:
461             ens = self._toBTC
462         return ens(n, addSign, wantTLA)
463
464     def humanToAmount(self, s):
465         ens = self.getNumberSystem()
466         m = re.search('\s*\\b(BTC|TBC)\s*$', s, re.IGNORECASE)
467         if m:
468             if m.group(1) == 'TBC':
469                 ens = 'Tonal'
470             else:
471                 ens = 'Decimal'
472             s = s[:m.start()]
473         if ens == 'Tonal':
474             ens = self._fromTBC
475         else:
476             ens = self._fromBTC
477         return ens(s)
478
479     def loadTranslator(self):
480         lang = _settings.value('language/language', '')
481         if not lang:
482             lang = os.getenv('LC_ALL') \
483                 or os.getenv('LC_MESSAGES') \
484                 or os.getenv('LANG')
485             if not lang:
486                 return
487         if not hasattr(self, 'translator'):
488             self.translator = QTranslator()
489         self.translator.load(lang, 'i18n')
490         app = QCoreApplication.instance()
491         app.installTranslator(self.translator)
492
493     debugMode = False
494
495 SpesmiloSettings = SpesmiloSettings()
496 format_number = SpesmiloSettings.format_number
497 humanAmount = SpesmiloSettings.humanAmount
498 humanToAmount = SpesmiloSettings.humanToAmount
499
500 import urllib
501 class _NotFancyURLopener(urllib.FancyURLopener):
502     def tr(self, s):
503         from PySide.QtGui import qApp
504         return qApp.translate('_NotFancyURLopener', s)
505
506     def prompt_user_passwd(self, host, realm):
507         raise NotImplementedError(self.tr("Wrong or missing username/password"))
508 urllib._urlopener = _NotFancyURLopener()
509
510 if __name__ == '__main__':
511     import sys
512     app = QApplication(sys.argv)
513     SpesmiloSettings.loadTranslator()
514     dlg = SettingsDialog(None)
515     sys.exit(app.exec_())