1 # -*- coding: utf-8 -*-
2 # Spesmilo -- Python Bitcoin user interface
3 # Copyright © 2011 Luke Dashjr
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.
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.
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/>.
17 from decimal import Decimal
21 from PySide.QtCore import *
22 from PySide.QtGui import *
26 _settings = QSettings('Bitcoin', 'Spesmilo')
30 if not hasattr(icon, '_default'):
31 icon._default = icon(*icon._defaultSearch)
34 ss += ('icons/%s.png' % (ss[0],),)
40 if QIcon.hasThemeIcon(s):
41 return QIcon.fromTheme(s)
43 icon._defaultSearch = ('spesmilo', 'bitcoin', 'icons/bitcoin32.png')
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
51 return subprocess.Popen(*args, **kwargs)
58 class SettingsTabBASE(QWidget):
59 def __init__(self, parent = None, dlg = None):
60 super(SettingsTabBASE, self).__init__(parent)
63 self.pp = parent.parent()
64 self.pphas = lambda x: hasattr(self.pp, x)
66 self.pphas = lambda x: False
72 nea = lambda: dlg.enableApply()
73 for o in self.options:
76 def loadSettings(self, settings = None):
77 for o in self.options:
78 o._CV = o._load(settings)
82 def checkSettings(self):
85 def saveSettings(self, settings = None):
87 for o in self.options:
89 if nv != o._CV and nv != o._OV:
91 if hasattr(o, '_apply'):
100 self._dlg.requireRestart()
102 class SettingsWidgetMixIn(object):
103 def __init__(self, *args, **kwargs):
104 self._key = kwargs['key']
105 self._default = kwargs['default']
107 del kwargs['default']
108 super(SettingsWidgetMixIn, self).__init__(*args, **kwargs)
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)
115 class SettingsQCheckBox(SettingsWidgetMixIn, QCheckBox):
116 def _onChange(self, slot):
117 self.stateChanged.connect(slot)
119 def _load(self, settings):
120 r = settings.value(self._key, None)
124 def _save(self, settings, newvalue):
125 settings.setValue(self._key, repr(newvalue))
128 return self.isChecked()
129 def _set(self, newvalue):
130 self.setChecked(newvalue)
132 class SettingsQComboBox(SettingsWidgetMixIn, QComboBox):
133 def _onChange(self, slot):
134 self.currentIndexChanged.connect(slot)
137 return self.itemData(self.currentIndex())
138 def _set(self, newvalue):
139 self.setCurrentIndex(self.findData(newvalue))
141 class SettingsQLineEdit(SettingsWidgetMixIn, QLineEdit):
142 def _onChange(self, slot):
143 self.textChanged.connect(slot)
147 def _set(self, newvalue):
148 self.setText(newvalue)
150 class SettingsTabCore(SettingsTabBASE):
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)
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)
165 mainlay = QVBoxLayout(self)
166 mainlay.addLayout(cblay)
167 mainlay.addLayout(lelay)
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)
175 def updateURIValidity(self):
176 en = not self.cbInternal.isChecked()
177 self.lblURI.setEnabled(en)
178 self.leURI.setEnabled(en)
180 class SettingsTabLanguage(SettingsTabBASE):
184 mainlay = QFormLayout(self)
186 self.lang = SettingsQComboBox(key='language/language', default='')
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'),
196 for lang in langlist:
197 if not (lang[1] in ('', 'en_US') or os.path.exists("i18n/%s.qm" % (lang[1],))):
199 self.lang.addItem(*lang)
200 self.options.append(self.lang)
201 mainlay.addRow(self.tr('Language:'), self.lang)
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)
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)
224 def _defer_update(self, *updates):
226 for update in updates:
227 update = 'update_%s' % (update,)
228 if self.pphas(update):
229 self._deferred[update] = True
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)()
240 class SettingsDialog(QDialog):
241 def __init__(self, parent):
242 super(SettingsDialog, self).__init__(parent)
244 self.settings = _settings
249 self.tabs.append((self.tr('Core'), SettingsTabCore(self, self)))
250 self.tabs.append((self.tr('Language'), SettingsTabLanguage(self, self)))
252 for name, widget in self.tabs:
253 tabw.addTab(widget, name)
255 mainlay = QVBoxLayout(self)
256 mainlay.addWidget(tabw)
258 actionlay = QHBoxLayout()
259 actionlay.addStretch()
261 okbtn = QPushButton(self.tr('&OK'))
262 okbtn.clicked.connect(self.accept)
263 okbtn.setAutoDefault(True)
264 actionlay.addWidget(okbtn)
266 applybtn = QPushButton(self.tr('&Apply'))
267 self.applybtn = applybtn
268 applybtn.clicked.connect(lambda: self.saveSettings())
269 actionlay.addWidget(applybtn)
271 cancelbtn = QPushButton(self.tr('&Cancel'))
272 cancelbtn.clicked.connect(self.reject)
273 actionlay.addWidget(cancelbtn)
275 mainlay.addLayout(actionlay)
280 self.accepted.connect(lambda: self.saveSettings())
282 self.setWindowIcon(icon())
283 self.setWindowTitle(self.tr('Settings'))
286 def loadSettings(self):
287 settings = self.settings
288 for x, widget in self.tabs:
289 widget.loadSettings(settings)
290 self.applybtn.setEnabled(False)
292 def checkSettings(self):
293 for x, widget in self.tabs:
294 widget.checkSettings()
296 def saveSettings(self):
297 settings = self.settings
298 for x, widget in self.tabs:
299 widget.saveSettings(settings)
300 self.applybtn.setEnabled(False)
302 def enableApply(self):
303 self.applybtn.setEnabled(True)
305 def requireRestart(self):
306 if not (hasattr(self.parent(), 'core') and self.parent().core):
308 msg = QMessageBox(QMessageBox.Information,
309 self.tr('Restart required'),
310 self.tr('Restarting Spesmilo is required for some changes to take effect.'))
313 class SpesmiloSettings:
314 def isConfigured(self):
315 NC = 'NOT CONFIGURED'
316 return _settings.value('core/internal', NC) != NC
318 def useInternalCore(self):
319 return _settings.value('core/internal', 'True') != 'False'
321 def getInternalCoreAuth(self):
322 if not hasattr(self, '_ICA'):
323 from random import random
325 self._ICA = ('spesmilo', passwd, 8342)
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')
333 def getNumberSystem(self):
334 return _settings.value('units/numsys', 'Decimal')
336 def getNumberSystemStrength(self):
337 return _settings.value('units/strength', 'Assume')
339 def getHideUnitTLA(self):
340 return _settings.value('units/hideTLA', 'True') != 'False'
342 def format_number(self, n, addSign = False, wantDelimiters = False):
343 ns = self.getNumberSystem()
345 n = anynumber.Tonal(n)
347 n = anynumber.Decimal(n)
348 return n.format(addSign=addSign, wantDelimiters=wantDelimiters)
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)
358 wantTLA = not self.getHideUnitTLA()
363 def _fromBTC(self, s):
364 s = anynumber.Decimal(s)
365 s = int(s * 100000000)
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)
372 wantTLA = not self.getHideUnitTLA()
377 def _fromTBC(self, s):
378 n = int(anynumber.Tonal(s) * 0x10000)
381 def ChooseUnits(self, n, guess = None):
384 ns = self.getNumberSystem()
385 nss = self.getNumberSystemStrength()
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
393 elif ivT and not ivD:
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:
402 dm = dn % 10 not in (0, 5)
403 tm = tn % 0x10 not in (0, 8)
412 if ens is None: ens = ns
415 def humanAmount(self, n, addSign = False, wantTLA = None):
416 ns = self.getNumberSystem()
418 ens = self.ChooseUnits(n)
427 return ens(n, addSign, wantTLA)
429 def humanToAmount(self, s):
430 ens = self.getNumberSystem()
431 m = re.search('\s*\\b(BTC|TBC)\s*$', s, re.IGNORECASE)
433 if m.group(1) == 'TBC':
444 def loadTranslator(self):
445 lang = _settings.value('language/language', '')
447 lang = os.getenv('LC_ALL') \
448 or os.getenv('LC_MESSAGES') \
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)
460 SpesmiloSettings = SpesmiloSettings()
461 format_number = SpesmiloSettings.format_number
462 humanAmount = SpesmiloSettings.humanAmount
463 humanToAmount = SpesmiloSettings.humanToAmount
466 class _NotFancyURLopener(urllib.FancyURLopener):
468 from PySide.QtGui import qApp
469 return qApp.translate('_NotFancyURLopener', s)
471 def prompt_user_passwd(self, host, realm):
472 raise NotImplementedError(self.tr("Wrong or missing username/password"))
473 urllib._urlopener = _NotFancyURLopener()
475 if __name__ == '__main__':
477 app = QApplication(sys.argv)
478 SpesmiloSettings.loadTranslator()
479 dlg = SettingsDialog(None)
480 sys.exit(app.exec_())