From 4111787ef3da5ddbcc565822878e4d932c7ddab1 Mon Sep 17 00:00:00 2001 From: "Wladimir J. van der Laan" Date: Tue, 18 Mar 2014 15:49:21 +0100 Subject: [PATCH] qt: Use locale-specific number formatting - Change bitcoinamountfield to use locale-specific number format - Change bitcoinunits to show and parse locale-specific numbers - If a language/territory is selected in options, this is set as default locale (overrides system locale). Fixes #3887 (cherry picked from commit laanwj/bitcoin@db0b8f3a2e96bc7c7aff95c9ade65be33dd27209, bitcoin/bitcoin#3893) --- src/qt/bitcoin.cpp | 3 ++ src/qt/bitcoinamountfield.cpp | 56 +++++++++++++++++------ src/qt/bitcoinunits.cpp | 85 +++++++++++++++++++++-------------- src/qt/bitcoinunits.h | 7 +-- src/qt/guiutil.cpp | 7 ++- 5 files changed, 106 insertions(+), 52 deletions(-) diff --git a/src/qt/bitcoin.cpp b/src/qt/bitcoin.cpp index e60a5f110..73546b741 100644 --- a/src/qt/bitcoin.cpp +++ b/src/qt/bitcoin.cpp @@ -94,6 +94,9 @@ static void initTranslations(QTranslator &qtTranslatorBase, QTranslator &qtTrans // 3) -lang command line argument lang_territory = QString::fromStdString(GetArg("-lang", lang_territory.toStdString())); + // Set default locale for amount and date formatting according to the selected language + QLocale::setDefault(QLocale(lang_territory)); + // Convert to "de" only by truncating "_DE" QString lang = lang_territory; lang.truncate(lang_territory.lastIndexOf('_')); diff --git a/src/qt/bitcoinamountfield.cpp b/src/qt/bitcoinamountfield.cpp index 25ad0c66a..3f4ca3e88 100644 --- a/src/qt/bitcoinamountfield.cpp +++ b/src/qt/bitcoinamountfield.cpp @@ -14,6 +14,46 @@ #include #include // for qPow() +/** QDoubleSpinBox that shows number group seperators. + * In Qt 5.3+ this could be replaced with QAbstractSpinBox::setGroupSeparatorShown(true) + * See https://bugreports.qt-project.org/browse/QTBUG-5142 + * + * TODO: We should not use a QDoubleSpinBox at all but implement our own + * spinbox for fixed-point numbers. + */ +class AmountSpinBox: public QDoubleSpinBox +{ +public: + explicit AmountSpinBox(QWidget *parent): + QDoubleSpinBox(parent) + { + } + QString textFromValue(double value) const + { + return QLocale().toString(value, 'f', decimals()); + } + QValidator::State validate (QString &text, int &pos) const + { + bool ok = false; + QValidator::State rv = QDoubleSpinBox::validate(text, pos); + if (rv == QValidator::Acceptable) + { + // Make sure that we only return acceptable if group seperators + // are in the right place. If not, a fixup step is needed first so + // return Intermediate. + QLocale().toDouble(text, &ok); + if (!ok) + return QValidator::Intermediate; + } + return rv; + } + + double valueFromText(const QString& text) const + { + return QLocale().toDouble(text); + } +}; + BitcoinAmountField::BitcoinAmountField(QWidget *parent) : QWidget(parent), amount(0), @@ -21,8 +61,7 @@ BitcoinAmountField::BitcoinAmountField(QWidget *parent) : { nSingleStep = 100000; // satoshis - amount = new QDoubleSpinBox(this); - amount->setLocale(QLocale::c()); + amount = new AmountSpinBox(this); amount->installEventFilter(this); amount->setMaximumWidth(170); @@ -52,7 +91,7 @@ void BitcoinAmountField::setText(const QString &text) if (text.isEmpty()) amount->clear(); else - amount->setValue(text.toDouble()); + amount->setValue(QLocale().toDouble(text)); } void BitcoinAmountField::clear() @@ -99,17 +138,6 @@ bool BitcoinAmountField::eventFilter(QObject *object, QEvent *event) // Clear invalid flag on focus setValid(true); } - else if (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease) - { - QKeyEvent *keyEvent = static_cast(event); - if (keyEvent->key() == Qt::Key_Comma) - { - // Translate a comma into a period - QKeyEvent periodKeyEvent(event->type(), Qt::Key_Period, keyEvent->modifiers(), ".", keyEvent->isAutoRepeat(), keyEvent->count()); - QApplication::sendEvent(object, &periodKeyEvent); - return true; - } - } return QWidget::eventFilter(object, event); } diff --git a/src/qt/bitcoinunits.cpp b/src/qt/bitcoinunits.cpp index 43120daef..f4225ab92 100644 --- a/src/qt/bitcoinunits.cpp +++ b/src/qt/bitcoinunits.cpp @@ -5,6 +5,7 @@ #include "bitcoinunits.h" #include +#include BitcoinUnits::BitcoinUnits(QObject *parent): QAbstractListModel(parent), @@ -108,71 +109,89 @@ int BitcoinUnits::decimals(int unit) } } -QString BitcoinUnits::format(int unit, qint64 n, bool fPlus) +QString BitcoinUnits::format(int unit, qint64 n, bool fPlus, bool fTrim, const QLocale &locale_in) { // Note: not using straight sprintf here because we do NOT want // localized number formatting. if(!valid(unit)) return QString(); // Refuse to format invalid unit + QLocale locale(locale_in); qint64 coin = factor(unit); int num_decimals = decimals(unit); + qint64 n_abs = (n > 0 ? n : -n); qint64 quotient = n_abs / coin; qint64 remainder = n_abs % coin; - QString quotient_str = QString::number(quotient); - QString remainder_str = QString::number(remainder).rightJustified(num_decimals, '0'); + // Quotient has group (decimal) separators if locale has this enabled + QString quotient_str = locale.toString(quotient); + // Remainder does not have group separators + locale.setNumberOptions(QLocale::OmitGroupSeparator | QLocale::RejectGroupSeparator); + QString remainder_str = locale.toString(remainder).rightJustified(num_decimals, '0'); - // Right-trim excess zeros after the decimal point - int nTrim = 0; - for (int i = remainder_str.size()-1; i>=2 && (remainder_str.at(i) == '0'); --i) - ++nTrim; - remainder_str.chop(nTrim); + if(fTrim) + { + // Right-trim excess zeros after the decimal point + int nTrim = 0; + for (int i = remainder_str.size()-1; i>=2 && (remainder_str.at(i) == '0'); --i) + ++nTrim; + remainder_str.chop(nTrim); + } if (n < 0) quotient_str.insert(0, '-'); - else if (fPlus && n > 0) + else if (fPlus && n >= 0) quotient_str.insert(0, '+'); - return quotient_str + QString(".") + remainder_str; + return quotient_str + locale.decimalPoint() + remainder_str; } -QString BitcoinUnits::formatWithUnit(int unit, qint64 amount, bool plussign) +QString BitcoinUnits::formatWithUnit(int unit, qint64 amount, bool plussign, bool trim, const QLocale &locale) { - return format(unit, amount, plussign) + QString(" ") + name(unit); + return format(unit, amount, plussign, trim) + QString(" ") + name(unit); } - -bool BitcoinUnits::parse(int unit, const QString &value, qint64 *val_out) +bool BitcoinUnits::parse(int unit, const QString &value, qint64 *val_out, const QLocale &locale_in) { if(!valid(unit) || value.isEmpty()) return false; // Refuse to parse invalid unit or empty string + + QLocale locale(locale_in); + qint64 coin = factor(unit); int num_decimals = decimals(unit); - QStringList parts = value.split("."); + QStringList parts = value.split(locale.decimalPoint()); + bool ok = false; if(parts.size() > 2) - { - return false; // More than one dot - } - QString whole = parts[0]; - QString decimals; + return false; // More than one decimal point + // Parse whole part (may include locale-specific group separators) +#if QT_VERSION < 0x050000 + qint64 whole = locale.toLongLong(parts[0], &ok, 10); +#else + qint64 whole = locale.toLongLong(parts[0], &ok); +#endif + if(!ok) + return false; // Parse error + if(whole > maxAmount(unit) || whole < 0) + return false; // Overflow or underflow + + // Parse decimals part (if present, may not include group separators) + qint64 decimals = 0; if(parts.size() > 1) { - decimals = parts[1]; + if(parts[1].size() > num_decimals) + return false; // Exceeds max precision + locale.setNumberOptions(QLocale::OmitGroupSeparator | QLocale::RejectGroupSeparator); +#if QT_VERSION < 0x050000 + decimals = locale.toLongLong(parts[1].leftJustified(num_decimals, '0'), &ok, 10); +#else + decimals = locale.toLongLong(parts[1].leftJustified(num_decimals, '0'), &ok); +#endif + if(!ok || decimals < 0) + return false; // Parse error } - if(decimals.size() > num_decimals) - { - return false; // Exceeds max precision - } - bool ok = false; - QString str = whole + decimals.leftJustified(num_decimals, '0'); - if(str.size() > 18) - { - return false; // Longer numbers will exceed 63 bits - } - qint64 retvalue = str.toLongLong(&ok); if(val_out) { - *val_out = retvalue; + *val_out = whole * coin + decimals; } return ok; } diff --git a/src/qt/bitcoinunits.h b/src/qt/bitcoinunits.h index 9f51b59b1..6d558f6d6 100644 --- a/src/qt/bitcoinunits.h +++ b/src/qt/bitcoinunits.h @@ -7,6 +7,7 @@ #include #include +#include /** Bitcoin unit definitions. Encapsulates parsing and formatting and serves as list model for drop-down selection boxes. @@ -52,11 +53,11 @@ public: //! Number of decimals left static int decimals(int unit); //! Format as string - static QString format(int unit, qint64 amount, bool plussign=false); + static QString format(int unit, qint64 amount, bool plussign=false, bool trim=true, const QLocale &locale=QLocale()); //! Format as string (with unit) - static QString formatWithUnit(int unit, qint64 amount, bool plussign=false); + static QString formatWithUnit(int unit, qint64 amount, bool plussign=false, bool trim=true, const QLocale &locale=QLocale()); //! Parse string to coin amount - static bool parse(int unit, const QString &value, qint64 *val_out); + static bool parse(int unit, const QString &value, qint64 *val_out, const QLocale &locale=QLocale()); ///@} //! @name AbstractListModel implementation diff --git a/src/qt/guiutil.cpp b/src/qt/guiutil.cpp index ad3d1b65a..d5ed397a8 100644 --- a/src/qt/guiutil.cpp +++ b/src/qt/guiutil.cpp @@ -58,7 +58,7 @@ namespace GUIUtil { QString dateTimeStr(const QDateTime &date) { - return date.date().toString(Qt::SystemLocaleShortDate) + QString(" ") + date.toString("hh:mm"); + return date.date().toString(Qt::DefaultLocaleShortDate) + QString(" ") + date.toString("hh:mm"); } QString dateTimeStr(qint64 nTime) @@ -133,7 +133,10 @@ bool parseBitcoinURI(const QUrl &uri, SendCoinsRecipient *out) { if(!i->second.isEmpty()) { - if(!BitcoinUnits::parse(BitcoinUnits::DOGE, i->second, &rv.amount)) + // Parse amount in C locale with no number separators + QLocale locale(QLocale::c()); + locale.setNumberOptions(QLocale::OmitGroupSeparator | QLocale::RejectGroupSeparator); + if(!BitcoinUnits::parse(BitcoinUnits::DOGE, i->second, &rv.amount, locale)) { return false; }