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 def style_item(item, style, want_old = False):
60 brush = item.foreground()
63 old = (brush.color(), font.style())
64 brush.setColor(style[0])
65 font.setStyle(style[1])
66 item.setForeground(brush)
70 disabled_style = (Qt.gray, QFont.StyleItalic)
71 def disable_item(item, *args, **kwargs):
72 return style_item(item, disabled_style, *args, **kwargs)
75 from PySide.QtGui import qApp
78 return QFontMetrics(qApp.font()).inFont(0xe9d9)
80 class SettingsTabBASE(QWidget):
81 def __init__(self, parent = None, dlg = None):
82 super(SettingsTabBASE, self).__init__(parent)
85 self.pp = parent.parent()
86 self.pphas = lambda x: hasattr(self.pp, x)
88 self.pphas = lambda x: False
94 nea = lambda: dlg.enableApply()
95 for o in self.options:
98 def loadSettings(self, settings = None):
99 for o in self.options:
100 o._CV = o._load(settings)
104 def checkSettings(self):
107 def saveSettings(self, settings = None):
109 for o in self.options:
111 if nv != o._CV and nv != o._OV:
113 if hasattr(o, '_apply'):
120 o._save(settings, nv)
122 self._dlg.requireRestart()
124 class SettingsWidgetMixIn(object):
125 def __init__(self, *args, **kwargs):
126 self._key = kwargs['key']
127 self._default = kwargs['default']
129 del kwargs['default']
130 super(SettingsWidgetMixIn, self).__init__(*args, **kwargs)
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)
137 class SettingsQCheckBox(SettingsWidgetMixIn, QCheckBox):
138 def _onChange(self, slot):
139 self.stateChanged.connect(slot)
141 def _load(self, settings):
142 r = settings.value(self._key, None)
146 def _save(self, settings, newvalue):
147 settings.setValue(self._key, repr(newvalue))
150 return self.isChecked()
151 def _set(self, newvalue):
152 self.setChecked(newvalue)
154 class SettingsQComboBox(SettingsWidgetMixIn, QComboBox):
155 def _onChange(self, slot):
156 self.currentIndexChanged.connect(slot)
159 return self.itemData(self.currentIndex())
160 def _set(self, newvalue):
161 self.setCurrentIndex(self.findData(newvalue))
163 class SettingsQLineEdit(SettingsWidgetMixIn, QLineEdit):
164 def _onChange(self, slot):
165 self.textChanged.connect(slot)
169 def _set(self, newvalue):
170 self.setText(newvalue)
172 class SettingsTabCore(SettingsTabBASE):
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)
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)
187 mainlay = QVBoxLayout(self)
188 mainlay.addLayout(cblay)
189 mainlay.addLayout(lelay)
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)
197 def updateURIValidity(self):
198 en = not self.cbInternal.isChecked()
199 self.lblURI.setEnabled(en)
200 self.leURI.setEnabled(en)
202 class SettingsTabLanguage(SettingsTabBASE):
206 mainlay = QFormLayout(self)
208 self.lang = SettingsQComboBox(key='language/language', default='')
209 langM = QStandardItemModel()
210 self.lang.setModel(langM)
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'),
220 for i in xrange(len(langlist)):
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)
227 self.options.append(self.lang)
228 mainlay.addRow(self.tr('Language:'), self.lang)
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)
248 self.options.append(self.numsys)
249 nslay.addWidget(self.numsys)
250 mainlay.addRow(self.tr('Number system:'), nslay)
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)
257 def _defer_update(self, *updates):
259 for update in updates:
260 update = 'update_%s' % (update,)
261 if self.pphas(update):
262 self._deferred[update] = True
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)()
273 class SettingsDialog(QDialog):
274 def __init__(self, parent):
275 super(SettingsDialog, self).__init__(parent)
277 self.settings = _settings
282 self.tabs.append((self.tr('Core'), SettingsTabCore(self, self)))
283 self.tabs.append((self.tr('Language'), SettingsTabLanguage(self, self)))
285 for name, widget in self.tabs:
286 tabw.addTab(widget, name)
288 mainlay = QVBoxLayout(self)
289 mainlay.addWidget(tabw)
291 actionlay = QHBoxLayout()
292 actionlay.addStretch()
294 okbtn = QPushButton(self.tr('&OK'))
295 okbtn.clicked.connect(self.accept)
296 okbtn.setAutoDefault(True)
297 actionlay.addWidget(okbtn)
299 applybtn = QPushButton(self.tr('&Apply'))
300 self.applybtn = applybtn
301 applybtn.clicked.connect(lambda: self.saveSettings())
302 actionlay.addWidget(applybtn)
304 cancelbtn = QPushButton(self.tr('&Cancel'))
305 cancelbtn.clicked.connect(self.reject)
306 actionlay.addWidget(cancelbtn)
308 mainlay.addLayout(actionlay)
313 self.accepted.connect(lambda: self.saveSettings())
315 self.setWindowIcon(icon())
316 self.setWindowTitle(self.tr('Settings'))
319 def loadSettings(self):
320 settings = self.settings
321 for x, widget in self.tabs:
322 widget.loadSettings(settings)
323 self.applybtn.setEnabled(False)
325 def checkSettings(self):
326 for x, widget in self.tabs:
327 widget.checkSettings()
329 def saveSettings(self):
330 settings = self.settings
331 for x, widget in self.tabs:
332 widget.saveSettings(settings)
333 self.applybtn.setEnabled(False)
335 def enableApply(self):
336 self.applybtn.setEnabled(True)
338 def requireRestart(self):
339 if not (hasattr(self.parent(), 'core') and self.parent().core):
341 msg = QMessageBox(QMessageBox.Information,
342 self.tr('Restart required'),
343 self.tr('Restarting Spesmilo is required for some changes to take effect.'))
346 class SpesmiloSettings:
347 def isConfigured(self):
348 NC = 'NOT CONFIGURED'
349 return _settings.value('core/internal', NC) != NC
351 def useInternalCore(self):
352 return _settings.value('core/internal', 'True') != 'False'
354 def getInternalCoreAuth(self):
355 if not hasattr(self, '_ICA'):
356 from random import random
358 self._ICA = ('spesmilo', passwd, 8342)
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')
366 def getNumberSystem(self):
367 if not TonalSupported():
369 return _settings.value('units/numsys', 'Decimal')
371 def getNumberSystemStrength(self):
372 return _settings.value('units/strength', 'Assume')
374 def getHideUnitTLA(self):
375 return _settings.value('units/hideTLA', 'True') != 'False'
377 def format_number(self, n, addSign = False, wantDelimiters = False):
378 ns = self.getNumberSystem()
380 n = anynumber.Tonal(n)
382 n = anynumber.Decimal(n)
383 return n.format(addSign=addSign, wantDelimiters=wantDelimiters)
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)
393 wantTLA = not self.getHideUnitTLA()
398 def _fromBTC(self, s):
399 s = anynumber.Decimal(s)
400 s = int(s * 100000000)
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)
407 wantTLA = not self.getHideUnitTLA()
412 def _fromTBC(self, s):
413 n = int(anynumber.Tonal(s) * 0x10000)
416 def ChooseUnits(self, n, guess = None):
419 ns = self.getNumberSystem()
420 nss = self.getNumberSystemStrength()
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
428 elif ivT and not ivD:
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:
437 dm = dn % 10 not in (0, 5)
438 tm = tn % 0x10 not in (0, 8)
447 if ens is None: ens = ns
450 def humanAmount(self, n, addSign = False, wantTLA = None):
451 ns = self.getNumberSystem()
453 ens = self.ChooseUnits(n)
462 return ens(n, addSign, wantTLA)
464 def humanToAmount(self, s):
465 ens = self.getNumberSystem()
466 m = re.search('\s*\\b(BTC|TBC)\s*$', s, re.IGNORECASE)
468 if m.group(1) == 'TBC':
479 def loadTranslator(self):
480 lang = _settings.value('language/language', '')
482 lang = os.getenv('LC_ALL') \
483 or os.getenv('LC_MESSAGES') \
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)
495 SpesmiloSettings = SpesmiloSettings()
496 format_number = SpesmiloSettings.format_number
497 humanAmount = SpesmiloSettings.humanAmount
498 humanToAmount = SpesmiloSettings.humanToAmount
501 class _NotFancyURLopener(urllib.FancyURLopener):
503 from PySide.QtGui import qApp
504 return qApp.translate('_NotFancyURLopener', s)
506 def prompt_user_passwd(self, host, realm):
507 raise NotImplementedError(self.tr("Wrong or missing username/password"))
508 urllib._urlopener = _NotFancyURLopener()
510 if __name__ == '__main__':
512 app = QApplication(sys.argv)
513 SpesmiloSettings.loadTranslator()
514 dlg = SettingsDialog(None)
515 sys.exit(app.exec_())