diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index 1237da65f7..fa3433f3cc 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -159,6 +159,7 @@ BITCOIN_QT_H = \ qt/sendcoinsrecipient.h \ qt/signverifymessagedialog.h \ qt/splashscreen.h \ + qt/tonalutils.h \ qt/trafficgraphwidget.h \ qt/transactiondesc.h \ qt/transactiondescdialog.h \ @@ -254,6 +255,7 @@ BITCOIN_QT_BASE_CPP = \ qt/qvaluecombobox.cpp \ qt/rpcconsole.cpp \ qt/splashscreen.cpp \ + qt/tonalutils.cpp \ qt/trafficgraphwidget.cpp \ qt/utilitydialog.cpp diff --git a/src/qt/bitcoinamountfield.cpp b/src/qt/bitcoinamountfield.cpp index a7e2d22488..09133ab7a2 100644 --- a/src/qt/bitcoinamountfield.cpp +++ b/src/qt/bitcoinamountfield.cpp @@ -94,7 +94,11 @@ public: { bool valid = false; CAmount val = value(&valid); - val = val + steps * singleStep; + CAmount currentSingleStep = singleStep; + if (!currentSingleStep) { + currentSingleStep = BitcoinUnits::singlestep(currentUnit); + } + val = val + steps * currentSingleStep; val = qBound(m_min_amount, val, m_max_amount); setValue(val); } @@ -151,7 +155,7 @@ public: private: BitcoinUnit currentUnit{BitcoinUnit::BTC}; - CAmount singleStep{CAmount(100000)}; // satoshis + CAmount singleStep{CAmount(0)}; mutable QSize cachedMinimumSizeHint; bool m_allow_empty{true}; CAmount m_min_amount{CAmount(0)}; diff --git a/src/qt/bitcoinunits.cpp b/src/qt/bitcoinunits.cpp index f73d83e4fc..2e3324cd64 100644 --- a/src/qt/bitcoinunits.cpp +++ b/src/qt/bitcoinunits.cpp @@ -3,6 +3,7 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include +#include #include @@ -11,6 +12,7 @@ #include static constexpr auto MAX_DIGITS_BTC = 16; +static constexpr auto MAX_DIGITS_TBC = 13; BitcoinUnits::BitcoinUnits(QObject *parent): QAbstractListModel(parent), @@ -25,6 +27,12 @@ QList BitcoinUnits::availableUnits() unitlist.append(Unit::mBTC); unitlist.append(Unit::uBTC); unitlist.append(Unit::SAT); + if (TonalUtils::Supported()) + { + unitlist.append(Unit::bTBC); + unitlist.append(Unit::sTBC); + unitlist.append(Unit::TBC); + } return unitlist; } @@ -35,6 +43,9 @@ QString BitcoinUnits::longName(Unit unit) case Unit::mBTC: return QString("mBTC"); case Unit::uBTC: return QString::fromUtf8("µBTC (bits)"); case Unit::SAT: return QString("Satoshi (sat)"); + case Unit::bTBC: return QString::fromUtf8("ᵇTBC"); + case Unit::sTBC: return QString::fromUtf8("ˢTBC"); + case Unit::TBC: return QString("TBC"); } // no default case, so the compiler can warn about missing cases assert(false); } @@ -46,6 +57,9 @@ QString BitcoinUnits::shortName(Unit unit) case Unit::mBTC: return longName(unit); case Unit::uBTC: return QString("bits"); case Unit::SAT: return QString("sat"); + case Unit::bTBC: return QString::fromUtf8("ᵇTBC"); + case Unit::sTBC: return QString::fromUtf8("ˢTBC"); + case Unit::TBC: return QString("TBC"); } // no default case, so the compiler can warn about missing cases assert(false); } @@ -53,10 +67,13 @@ QString BitcoinUnits::shortName(Unit unit) QString BitcoinUnits::description(Unit unit) { switch (unit) { - case Unit::BTC: return QString("Bitcoins"); + case Unit::BTC: return QString("Bitcoins (decimal)"); case Unit::mBTC: return QString("Milli-Bitcoins (1 / 1" THIN_SP_UTF8 "000)"); case Unit::uBTC: return QString("Micro-Bitcoins (bits) (1 / 1" THIN_SP_UTF8 "000" THIN_SP_UTF8 "000)"); case Unit::SAT: return QString("Satoshi (sat) (1 / 100" THIN_SP_UTF8 "000" THIN_SP_UTF8 "000)"); + case Unit::bTBC: return QString("Bong-Bitcoins (1,0000 tonal)"); + case Unit::sTBC: return QString("San-Bitcoins (100 tonal)"); + case Unit::TBC: return QString("Bitcoins (tonal)"); } // no default case, so the compiler can warn about missing cases assert(false); } @@ -68,6 +85,9 @@ qint64 BitcoinUnits::factor(Unit unit) case Unit::mBTC: return 100'000; case Unit::uBTC: return 100; case Unit::SAT: return 1; + case Unit::bTBC: return 0x100000000LL; + case Unit::sTBC: return 0x1000000; + case Unit::TBC: return 0x10000; } // no default case, so the compiler can warn about missing cases assert(false); } @@ -79,10 +99,57 @@ int BitcoinUnits::decimals(Unit unit) case Unit::mBTC: return 5; case Unit::uBTC: return 2; case Unit::SAT: return 0; + case Unit::bTBC: return 8; + case Unit::sTBC: return 6; + case Unit::TBC: return 4; } // no default case, so the compiler can warn about missing cases assert(false); } +int BitcoinUnits::radix(Unit unit) +{ + switch (unit) { + case Unit::bTBC: + case Unit::sTBC: + case Unit::TBC: + return 0x10; + default: + return 10; + } +} + +BitcoinUnit BitcoinUnits::numsys(Unit unit) +{ + switch (unit) { + case Unit::bTBC: + case Unit::sTBC: + case Unit::TBC: + return Unit::TBC; + default: + return Unit::BTC; + } +} + +qint64 BitcoinUnits::max_digits(Unit unit) +{ + switch (numsys(unit)) { + case Unit::TBC: + return MAX_DIGITS_TBC; + default: + return MAX_DIGITS_BTC; + } +} + +qint64 BitcoinUnits::singlestep(Unit unit) +{ + switch (numsys(unit)) { + case Unit::TBC: + return 0x10000; + default: + return 100000; + } +} + QString BitcoinUnits::format(Unit unit, const CAmount& nIn, bool fPlus, SeparatorStyle separators, bool justify) { // Note: not using straight sprintf here because we do NOT want @@ -92,11 +159,22 @@ QString BitcoinUnits::format(Unit unit, const CAmount& nIn, bool fPlus, Separato int num_decimals = decimals(unit); qint64 n_abs = (n > 0 ? n : -n); qint64 quotient = n_abs / coin; - QString quotient_str = QString::number(quotient); + int uradix = radix(unit); + QString quotient_str = QString::number(quotient, uradix); if (justify) { - quotient_str = quotient_str.rightJustified(MAX_DIGITS_BTC - num_decimals, ' '); + quotient_str = quotient_str.rightJustified(max_digits(unit) - num_decimals, ' '); } + QString remainder_str; + if (num_decimals > 0) { + const qint64 remainder = n_abs % coin; + remainder_str = QString::number(remainder, uradix).rightJustified(num_decimals, '0'); + } + + switch (numsys(unit)) { + case Unit::BTC: + { + // Use SI-style thin space separators as these are locale independent and can't be // confused with the decimal marker. QChar thin_sp(THIN_SP_CP); @@ -105,18 +183,28 @@ QString BitcoinUnits::format(Unit unit, const CAmount& nIn, bool fPlus, Separato for (int i = 3; i < q_size; i += 3) quotient_str.insert(q_size - i, thin_sp); + break; + } + case Unit::TBC: + { + // Right-trim excess zeros after the decimal point + static const QRegExp tail_zeros("0+$"); + remainder_str.remove(tail_zeros); + TonalUtils::ConvertFromHex(quotient_str); + TonalUtils::ConvertFromHex(remainder_str); + break; + } + default: assert(false); + } + if (n < 0) quotient_str.insert(0, '-'); else if (fPlus && n > 0) quotient_str.insert(0, '+'); - if (num_decimals > 0) { - qint64 remainder = n_abs % coin; - QString remainder_str = QString::number(remainder).rightJustified(num_decimals, '0'); - return quotient_str + QString(".") + remainder_str; - } else { - return quotient_str; - } + if (!remainder_str.isEmpty()) + quotient_str += QString(".") + remainder_str; + return quotient_str; } @@ -180,11 +268,18 @@ bool BitcoinUnits::parse(Unit unit, const QString& value, CAmount* val_out) bool ok = false; QString str = whole + decimals.leftJustified(num_decimals, '0'); + Unit unumsys = numsys(unit); + if (unumsys == Unit::TBC) { + if (str.size() > 15) + return false; // Longer numbers may exceed 63 bits + TonalUtils::ConvertToHex(str); + } else if(str.size() > 18) { return false; // Longer numbers will exceed 63 bits } - CAmount retvalue(str.toLongLong(&ok)); + + CAmount retvalue(str.toLongLong(&ok, radix(unit))); if(val_out) { *val_out = retvalue; @@ -228,18 +323,21 @@ CAmount BitcoinUnits::maxMoney() return MAX_MONEY; } -namespace { -qint8 ToQint8(BitcoinUnit unit) +std::variant BitcoinUnits::ToSetting(BitcoinUnit unit) { switch (unit) { - case BitcoinUnit::BTC: return 0; - case BitcoinUnit::mBTC: return 1; - case BitcoinUnit::uBTC: return 2; - case BitcoinUnit::SAT: return 3; + case BitcoinUnit::BTC: return qint8{0}; + case BitcoinUnit::mBTC: return qint8{1}; + case BitcoinUnit::uBTC: return qint8{2}; + case BitcoinUnit::SAT: return qint8{3}; + case BitcoinUnit::bTBC: return QString("bTBC"); + case BitcoinUnit::sTBC: return QString("sTBC"); + case BitcoinUnit::TBC: return QString("TBC"); } // no default case, so the compiler can warn about missing cases assert(false); } +namespace { BitcoinUnit FromQint8(qint8 num) { switch (num) { @@ -248,13 +346,32 @@ BitcoinUnit FromQint8(qint8 num) case 2: return BitcoinUnit::uBTC; case 3: return BitcoinUnit::SAT; } - assert(false); + return BitcoinUnit::BTC; } } // namespace +BitcoinUnit BitcoinUnits::FromSetting(const QString& s, BitcoinUnit def) +{ + if (s == "0") return BitcoinUnit::BTC; + if (s == "1") return BitcoinUnit::mBTC; + if (s == "2") return BitcoinUnit::uBTC; + if (s == "3") return BitcoinUnit::SAT; + if (s == "4") return BitcoinUnit::sTBC; + if (s == "5") return BitcoinUnit::TBC; + if (s == "bTBC") return BitcoinUnit::bTBC; + if (s == "sTBC") return BitcoinUnit::sTBC; + if (s == "TBC") return BitcoinUnit::TBC; + return def; +} + QDataStream& operator<<(QDataStream& out, const BitcoinUnit& unit) { - return out << ToQint8(unit); + auto setting_val = BitcoinUnits::ToSetting(unit); + if (const QString* setting_str = std::get_if(&setting_val)) { + return out << qint8{0} << *setting_str; + } else { + return out << std::get(setting_val); + } } QDataStream& operator>>(QDataStream& in, BitcoinUnit& unit) @@ -262,5 +379,10 @@ QDataStream& operator>>(QDataStream& in, BitcoinUnit& unit) qint8 input; in >> input; unit = FromQint8(input); + if (!in.atEnd()) { + QString setting_str; + in >> setting_str; + unit = BitcoinUnits::FromSetting(setting_str, unit); + } return in; } diff --git a/src/qt/bitcoinunits.h b/src/qt/bitcoinunits.h index b3b5a8fc18..4e9e77cee0 100644 --- a/src/qt/bitcoinunits.h +++ b/src/qt/bitcoinunits.h @@ -43,7 +43,10 @@ public: BTC, mBTC, uBTC, - SAT + SAT, + bTBC, + sTBC, + TBC, }; Q_ENUM(Unit) @@ -60,6 +63,10 @@ public: //! Get list of units, for drop-down box static QList availableUnits(); + //! String for setting(s) + static std::variant ToSetting(Unit unit); + //! Convert setting(s) string to unit + static Unit FromSetting(const QString&, Unit def); //! Long name static QString longName(Unit unit); //! Short name @@ -68,8 +75,16 @@ public: static QString description(Unit unit); //! Number of Satoshis (1e-8) per unit static qint64 factor(Unit unit); - //! Number of decimals left + //! Number of fractional places static int decimals(Unit unit); + //! Radix + static int radix(Unit unit); + //! Number system + static Unit numsys(Unit unit); + //! Number of digits total in maximum value + static qint64 max_digits(Unit unit); + //! "Single step" amount, in satoshis + static qint64 singlestep(Unit unit); //! Format as string static QString format(Unit unit, const CAmount& amount, bool plussign = false, SeparatorStyle separators = SeparatorStyle::STANDARD, bool justify = false); //! Format as string (with unit) diff --git a/src/qt/optionsmodel.cpp b/src/qt/optionsmodel.cpp index 7d2a82d2d2..27c47172ed 100644 --- a/src/qt/optionsmodel.cpp +++ b/src/qt/optionsmodel.cpp @@ -245,9 +245,17 @@ bool OptionsModel::Init(bilingual_str& error) // Display if (!settings.contains("DisplayBitcoinUnit")) { - settings.setValue("DisplayBitcoinUnit", QVariant::fromValue(BitcoinUnit::BTC)); + auto init_unit = BitcoinUnit::BTC; + if (settings.contains("nDisplayUnit")) { + // Migrate to new setting + init_unit = BitcoinUnits::FromSetting(settings.value("nDisplayUnit").toString(), init_unit); + } + settings.setValue("DisplayBitcoinUnit", QVariant::fromValue(init_unit)); } QVariant unit = settings.value("DisplayBitcoinUnit"); + if (settings.contains("DisplayBitcoinUnitKnots")) { + unit = settings.value("DisplayBitcoinUnitKnots"); + } if (unit.canConvert()) { m_display_bitcoin_unit = unit.value(); } else { @@ -919,7 +927,21 @@ void OptionsModel::setDisplayUnit(const QVariant& new_unit) if (new_unit.isNull() || new_unit.value() == m_display_bitcoin_unit) return; m_display_bitcoin_unit = new_unit.value(); QSettings settings; - settings.setValue("DisplayBitcoinUnit", QVariant::fromValue(m_display_bitcoin_unit)); + if (BitcoinUnits::numsys(m_display_bitcoin_unit) == BitcoinUnit::BTC) { + settings.setValue("DisplayBitcoinUnit", QVariant::fromValue(m_display_bitcoin_unit)); + settings.remove("DisplayBitcoinUnitKnots"); + } else { + settings.setValue("DisplayBitcoinUnitKnots", QVariant::fromValue(m_display_bitcoin_unit)); + } + { + // For older versions: + auto setting_val = BitcoinUnits::ToSetting(m_display_bitcoin_unit); + if (const QString* setting_str = std::get_if(&setting_val)) { + settings.setValue("nDisplayUnit", *setting_str); + } else { + settings.setValue("nDisplayUnit", std::get(setting_val)); + } + } Q_EMIT displayUnitChanged(m_display_bitcoin_unit); } diff --git a/src/qt/tonalutils.cpp b/src/qt/tonalutils.cpp new file mode 100644 index 0000000000..4d6767cb91 --- /dev/null +++ b/src/qt/tonalutils.cpp @@ -0,0 +1,83 @@ +// Copyright (c) 2016 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +#include +#include +#include +#include +#include + +static const QList tonal_digits{0xe8ef, 0xe8ee, 0xe8ed, 0xe8ec, 0xe8eb, 0xe8ea, 0xe8e9, '8', '7', '6', '5', '4', '3', '2', '1', '0'}; + +namespace { + +bool font_supports_tonal(const QFont& font) +{ + const QFontMetrics fm(font); + QString s = "000"; + const QSize sz = fm.size(0, s); + for (const auto& c : tonal_digits) { + if (!fm.inFont(c)) return false; + s[0] = s[1] = s[2] = c; + if (sz != fm.size(0, s)) return false; + } + return true; +} + +} // anon namespace + +bool TonalUtils::Supported() +{ + QFont default_font; + if (font_supports_tonal(default_font)) return true; + QFont last_resort_font(default_font.lastResortFamily()); + if (font_supports_tonal(last_resort_font)) return true; + return false; +} + +#define RE_TONAL_DIGIT "[\\d\\xe8e0-\\xe8ef\\xe9d0-\\xe9df]" +static QRegExpValidator tv(QRegExp("-?(?:" RE_TONAL_DIGIT "+\\.?|" RE_TONAL_DIGIT "*\\." RE_TONAL_DIGIT "+)"), nullptr); + +QValidator::State TonalUtils::validate(QString&input, int&pos) +{ + return tv.validate(input, pos); +} + +void TonalUtils::ConvertFromHex(QString&str) +{ + for (int i = 0; i < str.size(); ++i) + { + ushort c = str[i].unicode(); + if (c == '9') + str[i] = 0xe8e9; + else + if (c >= 'A' && c <= 'F') + str[i] = c + (0xe8ea - 'A'); + else + if (c >= 'a' && c <= 'f') + str[i] = c + (0xe8ea - 'a'); + } +} + +void TonalUtils::ConvertToHex(QString&str) +{ + for (int i = 0; i < str.size(); ++i) + { + ushort c = str[i].unicode(); + if (c == '9') + str[i] = 'a'; + else + if (c >= 0xe8e0 && c <= 0xe8e9) { // UCSUR 0-9 + str[i] = c - (0xe8e0 - '0'); + } else if (c >= 0xe8ea && c <= 0xe8ef) { // UCSUR a-f + str[i] = c - (0xe8ea - 'a'); + } else if (c >= 0xe9d0 && c <= 0xe9d9) { + str[i] = c - (0xe9d0 - '0'); + } else + if (c >= 0xe9da && c <= 0xe9df) + str[i] = c - 0xe999; + } +} diff --git a/src/qt/tonalutils.h b/src/qt/tonalutils.h new file mode 100644 index 0000000000..eb6786b765 --- /dev/null +++ b/src/qt/tonalutils.h @@ -0,0 +1,25 @@ +// Copyright (c) 2016 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_QT_TONALUTILS_H +#define BITCOIN_QT_TONALUTILS_H + +#include + +QT_BEGIN_NAMESPACE +class QString; +QT_END_NAMESPACE + +class TonalUtils +{ +public: + static bool Supported(); + + static QValidator::State validate(QString&input, int&pos); + + static void ConvertFromHex(QString&); + static void ConvertToHex(QString&); +}; + +#endif // BITCOIN_QT_TONALUTILS_H