make non-English languages region-independent
[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 class SettingsTabBASE(QWidget):
59     def __init__(self, parent = None, dlg = None):
60         super(SettingsTabBASE, self).__init__(parent)
61
62         if parent:
63             self.pp = parent.parent()
64             self.pphas = lambda x: hasattr(self.pp, x)
65         else:
66             self.pphas = lambda x: False
67
68         self.options = []
69         self._build()
70
71         self._dlg = dlg
72         nea = lambda: dlg.enableApply()
73         for o in self.options:
74             o._onChange(nea)
75
76     def loadSettings(self, settings = None):
77         for o in self.options:
78             o._CV = o._load(settings)
79             o._OV = o._CV
80             o._set(o._CV)
81
82     def checkSettings(self):
83         pass
84
85     def saveSettings(self, settings = None):
86         RR = False
87         for o in self.options:
88             nv = o._get()
89             if nv != o._CV and nv != o._OV:
90                 o._CV = nv
91                 if hasattr(o, '_apply'):
92                     if o._apply(nv):
93                         RR = True
94                     else:
95                         o._OV = nv
96                 else:
97                     RR = True
98             o._save(settings, nv)
99         if RR:
100             self._dlg.requireRestart()
101
102 class SettingsWidgetMixIn(object):
103     def __init__(self, *args, **kwargs):
104         self._key = kwargs['key']
105         self._default = kwargs['default']
106         del kwargs['key']
107         del kwargs['default']
108         super(SettingsWidgetMixIn, self).__init__(*args, **kwargs)
109
110     def _load(self, settings):
111         return settings.value(self._key, self._default)
112     def _save(self, settings, newvalue):
113         settings.setValue(self._key, newvalue)
114
115 class SettingsQCheckBox(SettingsWidgetMixIn, QCheckBox):
116     def _onChange(self, slot):
117         self.stateChanged.connect(slot)
118
119     def _load(self, settings):
120         r = settings.value(self._key, None)
121         if r is None:
122             return self._default
123         return r != 'False'
124     def _save(self, settings, newvalue):
125         settings.setValue(self._key, repr(newvalue))
126
127     def _get(self):
128         return self.isChecked()
129     def _set(self, newvalue):
130         self.setChecked(newvalue)
131
132 class SettingsQComboBox(SettingsWidgetMixIn, QComboBox):
133     def _onChange(self, slot):
134         self.currentIndexChanged.connect(slot)
135
136     def _get(self):
137         return self.itemData(self.currentIndex())
138     def _set(self, newvalue):
139         self.setCurrentIndex(self.findData(newvalue))
140
141 class SettingsQLineEdit(SettingsWidgetMixIn, QLineEdit):
142     def _onChange(self, slot):
143         self.textChanged.connect(slot)
144
145     def _get(self):
146         return self.text()
147     def _set(self, newvalue):
148         self.setText(newvalue)
149
150 class SettingsTabCore(SettingsTabBASE):
151     def _build(self):
152         cblay = QFormLayout()
153         self.cbInternal = SettingsQCheckBox(self, key='core/internal', default=True)
154         self.options.append(self.cbInternal)
155         self.lblInternal = QLabel(self.tr('Use internal core'))
156         cblay.addRow(self.cbInternal, self.lblInternal)
157         self.cbInternal.stateChanged.connect(self.updateURIValidity)
158         
159         lelay = QFormLayout()
160         self.lblURI = QLabel(self.tr('URI:'))
161         self.leURI = SettingsQLineEdit(key='core/uri', default='http://user:pass@localhost:8332')
162         self.options.append(self.leURI)
163         lelay.addRow(self.lblURI, self.leURI)
164
165         mainlay = QVBoxLayout(self)
166         mainlay.addLayout(cblay)
167         mainlay.addLayout(lelay)
168     
169     def checkSettings(self):
170         if quietPopen( ('bitcoind', '--help') ).wait():
171             self.cbInternal.setChecked(False)
172             self.cbInternal.setEnabled(False)
173             self.lblInternal.setEnabled(False)
174     
175     def updateURIValidity(self):
176         en = not self.cbInternal.isChecked()
177         self.lblURI.setEnabled(en)
178         self.leURI.setEnabled(en)
179
180 class SettingsTabLanguage(SettingsTabBASE):
181     def _build(self):
182         self._deferred = {}
183         
184         mainlay = QFormLayout(self)
185         
186         self.lang = SettingsQComboBox(key='language/language', default='')
187         langlist = [
188             (self.tr('(Default)'), ''),
189             (self.tr('American'), 'en_US'),
190             (self.tr('English'), 'en_GB'),
191             (self.tr('Esperanto'), 'eo'),
192             (self.tr('Dutch'), 'nl'),
193             (self.tr('French'), 'fr'),
194         ]
195         langlist.sort()
196         for lang in langlist:
197             if not (lang[1] in ('', 'en_US') or os.path.exists("i18n/%s.qm" % (lang[1],))):
198                 continue
199             self.lang.addItem(*lang)
200         self.options.append(self.lang)
201         mainlay.addRow(self.tr('Language:'), self.lang)
202         
203         nslay = QHBoxLayout()
204         self.strength = SettingsQComboBox(key='units/strength', default='Assume')
205         self.strength._apply = lambda nv: self._defer_update('amounts')
206         self.strength.addItem(self.tr('Assume'), 'Assume')
207         self.strength.addItem(self.tr('Prefer'), 'Prefer')
208         self.strength.addItem(self.tr('Force'), 'Force')
209         self.options.append(self.strength)
210         nslay.addWidget(self.strength)
211         self.numsys = SettingsQComboBox(self, key='units/numsys', default='Decimal')
212         self.numsys._apply = lambda nv: self._defer_update('counters', 'amounts')
213         self.numsys.addItem(self.tr('Decimal'), 'Decimal')
214         self.numsys.addItem(self.tr('Tonal'), 'Tonal')
215         self.options.append(self.numsys)
216         nslay.addWidget(self.numsys)
217         mainlay.addRow(self.tr('Number system:'), nslay)
218         
219         self.hideTLA = SettingsQCheckBox(self.tr('Hide preferred unit name'), key='units/hideTLA', default=True)
220         self.hideTLA._apply = lambda nv: self._defer_update('amounts')
221         self.options.append(self.hideTLA)
222         mainlay.addRow(self.hideTLA)
223     
224     def _defer_update(self, *updates):
225         RR = False
226         for update in updates:
227             update = 'update_%s' % (update,)
228             if self.pphas(update):
229                 self._deferred[update] = True
230             else:
231                 RR = True
232         return RR
233
234     def saveSettings(self, *args, **kwargs):
235         super(SettingsTabLanguage, self).saveSettings(*args, **kwargs)
236         for du in self._deferred.iterkeys():
237             getattr(self.pp, du)()
238         self._deferred = {}
239
240 class SettingsDialog(QDialog):
241     def __init__(self, parent):
242         super(SettingsDialog, self).__init__(parent)
243         
244         self.settings = _settings
245         
246         tabw = QTabWidget()
247         
248         self.tabs = []
249         self.tabs.append((self.tr('Core'), SettingsTabCore(self, self)))
250         self.tabs.append((self.tr('Language'), SettingsTabLanguage(self, self)))
251         
252         for name, widget in self.tabs:
253             tabw.addTab(widget, name)
254         
255         mainlay = QVBoxLayout(self)
256         mainlay.addWidget(tabw)
257         
258         actionlay = QHBoxLayout()
259         actionlay.addStretch()
260         
261         okbtn = QPushButton(self.tr('&OK'))
262         okbtn.clicked.connect(self.accept)
263         okbtn.setAutoDefault(True)
264         actionlay.addWidget(okbtn)
265         
266         applybtn = QPushButton(self.tr('&Apply'))
267         self.applybtn = applybtn
268         applybtn.clicked.connect(lambda: self.saveSettings())
269         actionlay.addWidget(applybtn)
270         
271         cancelbtn = QPushButton(self.tr('&Cancel'))
272         cancelbtn.clicked.connect(self.reject)
273         actionlay.addWidget(cancelbtn)
274
275         mainlay.addLayout(actionlay)
276         
277         self.loadSettings()
278         self.checkSettings()
279         
280         self.accepted.connect(lambda: self.saveSettings())
281         
282         self.setWindowIcon(icon())
283         self.setWindowTitle(self.tr('Settings'))
284         self.show()
285     
286     def loadSettings(self):
287         settings = self.settings
288         for x, widget in self.tabs:
289             widget.loadSettings(settings)
290         self.applybtn.setEnabled(False)
291     
292     def checkSettings(self):
293         for x, widget in self.tabs:
294             widget.checkSettings()
295         
296     def saveSettings(self):
297         settings = self.settings
298         for x, widget in self.tabs:
299             widget.saveSettings(settings)
300         self.applybtn.setEnabled(False)
301
302     def enableApply(self):
303         self.applybtn.setEnabled(True)
304
305     def requireRestart(self):
306         if not (hasattr(self.parent(), 'core') and self.parent().core):
307             return
308         msg = QMessageBox(QMessageBox.Information,
309                           self.tr('Restart required'),
310                           self.tr('Restarting Spesmilo is required for some changes to take effect.'))
311         msg.exec_()
312
313 class SpesmiloSettings:
314     def isConfigured(self):
315         NC = 'NOT CONFIGURED'
316         return _settings.value('core/internal', NC) != NC
317     
318     def useInternalCore(self):
319         return _settings.value('core/internal', 'True') != 'False'
320     
321     def getInternalCoreAuth(self):
322         if not hasattr(self, '_ICA'):
323             from random import random
324             passwd = random()
325             self._ICA = ('spesmilo', passwd, 8342)
326         return self._ICA
327
328     def getEffectiveURI(self):
329         if self.useInternalCore():
330             return 'http://%s:%s@127.0.0.1:%d' % self.getInternalCoreAuth()
331         return _settings.value('core/uri', 'http://user:pass@localhost:8332')
332
333     def getNumberSystem(self):
334         return _settings.value('units/numsys', 'Decimal')
335
336     def getNumberSystemStrength(self):
337         return _settings.value('units/strength', 'Assume')
338
339     def getHideUnitTLA(self):
340         return _settings.value('units/hideTLA', 'True') != 'False'
341
342     def format_number(self, n, addSign = False, wantDelimiters = False):
343         ns = self.getNumberSystem()
344         if ns == 'Tonal':
345             n = anynumber.Tonal(n)
346         else:
347             n = anynumber.Decimal(n)
348         return n.format(addSign=addSign, wantDelimiters=wantDelimiters)
349
350     def _toBTC(self, n, addSign = False, wantTLA = None, wantDelimiters = False):
351         n = anynumber.Decimal(n) / 100000000
352         s = n.format(addSign=addSign, wantDelimiters=wantDelimiters)
353         if '.' not in s:
354             s += '.00'
355         elif s[-2] == '.':
356             s += '0'
357         if wantTLA is None:
358             wantTLA = not self.getHideUnitTLA()
359         if wantTLA:
360             s += " BTC"
361         return s
362
363     def _fromBTC(self, s):
364         s = anynumber.Decimal(s)
365         s = int(s * 100000000)
366         return s
367
368     def _toTBC(self, n, addSign = False, wantTLA = None, wantDelimiters = False):
369         n = anynumber.Tonal(n) / 0x10000
370         s = n.format(addSign=addSign, wantDelimiters=wantDelimiters)
371         if wantTLA is None:
372             wantTLA = not self.getHideUnitTLA()
373         if wantTLA:
374             s += " TBC"
375         return s
376
377     def _fromTBC(self, s):
378         n = int(anynumber.Tonal(s) * 0x10000)
379         return n
380
381     def ChooseUnits(self, n, guess = None):
382         if float(n) != n:
383             raise ValueError()
384         ns = self.getNumberSystem()
385         nss = self.getNumberSystemStrength()
386         ens = None
387         if nss != 'Force' and n:
388             # If it's only valid as one, and not the other, choose it
389             ivD = 0 == n % 1000000
390             ivT = 0 == n % 0x100
391             if ivD and not ivT:
392                 ens = 'Decimal'
393             elif ivT and not ivD:
394                 ens = 'Tonal'
395             # If it could be either, pick the more likely one (only with 'Assume')
396             elif ivD and nss == 'Assume':
397                 if not guess is None:
398                     ens = guess
399                 dn = n / 1000000
400                 tn = n / 0x100
401                 while ens is None:
402                     dm = dn % 10 not in (0, 5)
403                     tm = tn % 0x10 not in (0, 8)
404                     if dm:
405                         if tm:
406                             break
407                         ens = 'Tonal'
408                     elif tm:
409                         ens = 'Decimal'
410                     dn /= 10.
411                     tn /= 16.
412         if ens is None: ens = ns
413         return ens
414
415     def humanAmount(self, n, addSign = False, wantTLA = None):
416         ns = self.getNumberSystem()
417         try:
418             ens = self.ChooseUnits(n)
419         except ValueError:
420             return n
421         if ens != ns:
422             wantTLA = True
423         if ens == 'Tonal':
424             ens = self._toTBC
425         else:
426             ens = self._toBTC
427         return ens(n, addSign, wantTLA)
428
429     def humanToAmount(self, s):
430         ens = self.getNumberSystem()
431         m = re.search('\s*\\b(BTC|TBC)\s*$', s, re.IGNORECASE)
432         if m:
433             if m.group(1) == 'TBC':
434                 ens = 'Tonal'
435             else:
436                 ens = 'Decimal'
437             s = s[:m.start()]
438         if ens == 'Tonal':
439             ens = self._fromTBC
440         else:
441             ens = self._fromBTC
442         return ens(s)
443
444     def loadTranslator(self):
445         lang = _settings.value('language/language', '')
446         if not lang:
447             lang = os.getenv('LC_ALL') \
448                 or os.getenv('LC_MESSAGES') \
449                 or os.getenv('LANG')
450             if not lang:
451                 return
452         if not hasattr(self, 'translator'):
453             self.translator = QTranslator()
454         self.translator.load(lang, 'i18n')
455         app = QCoreApplication.instance()
456         app.installTranslator(self.translator)
457
458     debugMode = False
459
460 SpesmiloSettings = SpesmiloSettings()
461 format_number = SpesmiloSettings.format_number
462 humanAmount = SpesmiloSettings.humanAmount
463 humanToAmount = SpesmiloSettings.humanToAmount
464
465 import urllib
466 class _NotFancyURLopener(urllib.FancyURLopener):
467     def tr(self, s):
468         from PySide.QtGui import qApp
469         return qApp.translate('_NotFancyURLopener', s)
470
471     def prompt_user_passwd(self, host, realm):
472         raise NotImplementedError(self.tr("Wrong or missing username/password"))
473 urllib._urlopener = _NotFancyURLopener()
474
475 if __name__ == '__main__':
476     import sys
477     app = QApplication(sys.argv)
478     SpesmiloSettings.loadTranslator()
479     dlg = SettingsDialog(None)
480     sys.exit(app.exec_())