bitcoinamountfield.cpp
1 // Copyright (c) 2011-present The Bitcoin Core developers 2 // Distributed under the MIT software license, see the accompanying 3 // file COPYING or http://www.opensource.org/licenses/mit-license.php. 4 5 #include <qt/bitcoinamountfield.h> 6 7 #include <qt/bitcoinunits.h> 8 #include <qt/guiconstants.h> 9 #include <qt/guiutil.h> 10 #include <qt/qvaluecombobox.h> 11 12 #include <QApplication> 13 #include <QAbstractSpinBox> 14 #include <QHBoxLayout> 15 #include <QKeyEvent> 16 #include <QLineEdit> 17 #include <QVariant> 18 19 #include <cassert> 20 21 /** QSpinBox that uses fixed-point numbers internally and uses our own 22 * formatting/parsing functions. 23 */ 24 class AmountSpinBox: public QAbstractSpinBox 25 { 26 Q_OBJECT 27 28 public: 29 explicit AmountSpinBox(QWidget *parent): 30 QAbstractSpinBox(parent) 31 { 32 setAlignment(Qt::AlignRight); 33 34 connect(lineEdit(), &QLineEdit::textEdited, this, &AmountSpinBox::valueChanged); 35 } 36 37 QValidator::State validate(QString &text, int &pos) const override 38 { 39 if(text.isEmpty()) 40 return QValidator::Intermediate; 41 bool valid = false; 42 parse(text, &valid); 43 /* Make sure we return Intermediate so that fixup() is called on defocus */ 44 return valid ? QValidator::Intermediate : QValidator::Invalid; 45 } 46 47 void fixup(QString &input) const override 48 { 49 bool valid; 50 CAmount val; 51 52 if (input.isEmpty() && !m_allow_empty) { 53 valid = true; 54 val = m_min_amount; 55 } else { 56 valid = false; 57 val = parse(input, &valid); 58 } 59 60 if (valid) { 61 val = qBound(m_min_amount, val, m_max_amount); 62 input = BitcoinUnits::format(currentUnit, val, false, BitcoinUnits::SeparatorStyle::ALWAYS); 63 lineEdit()->setText(input); 64 } 65 } 66 67 CAmount value(bool *valid_out=nullptr) const 68 { 69 return parse(text(), valid_out); 70 } 71 72 void setValue(const CAmount& value) 73 { 74 lineEdit()->setText(BitcoinUnits::format(currentUnit, value, false, BitcoinUnits::SeparatorStyle::ALWAYS)); 75 Q_EMIT valueChanged(); 76 } 77 78 void SetAllowEmpty(bool allow) 79 { 80 m_allow_empty = allow; 81 } 82 83 void SetMinValue(const CAmount& value) 84 { 85 m_min_amount = value; 86 } 87 88 void SetMaxValue(const CAmount& value) 89 { 90 m_max_amount = value; 91 } 92 93 void stepBy(int steps) override 94 { 95 bool valid = false; 96 CAmount val = value(&valid); 97 val = val + steps * singleStep; 98 val = qBound(m_min_amount, val, m_max_amount); 99 setValue(val); 100 } 101 102 void setDisplayUnit(BitcoinUnit unit) 103 { 104 bool valid = false; 105 CAmount val = value(&valid); 106 107 currentUnit = unit; 108 lineEdit()->setPlaceholderText(BitcoinUnits::format(currentUnit, m_min_amount, false, BitcoinUnits::SeparatorStyle::ALWAYS)); 109 if(valid) 110 setValue(val); 111 else 112 clear(); 113 } 114 115 void setSingleStep(const CAmount& step) 116 { 117 singleStep = step; 118 } 119 120 QSize minimumSizeHint() const override 121 { 122 if(cachedMinimumSizeHint.isEmpty()) 123 { 124 ensurePolished(); 125 126 const QFontMetrics fm(fontMetrics()); 127 int h = lineEdit()->minimumSizeHint().height(); 128 int w = GUIUtil::TextWidth(fm, BitcoinUnits::format(BitcoinUnit::BTC, BitcoinUnits::maxMoney(), false, BitcoinUnits::SeparatorStyle::ALWAYS)); 129 w += 2; // cursor blinking space 130 131 QStyleOptionSpinBox opt; 132 initStyleOption(&opt); 133 QSize hint(w, h); 134 QSize extra(35, 6); 135 opt.rect.setSize(hint + extra); 136 extra += hint - style()->subControlRect(QStyle::CC_SpinBox, &opt, 137 QStyle::SC_SpinBoxEditField, this).size(); 138 // get closer to final result by repeating the calculation 139 opt.rect.setSize(hint + extra); 140 extra += hint - style()->subControlRect(QStyle::CC_SpinBox, &opt, 141 QStyle::SC_SpinBoxEditField, this).size(); 142 hint += extra; 143 hint.setHeight(h); 144 145 opt.rect = rect(); 146 147 cachedMinimumSizeHint = style()->sizeFromContents(QStyle::CT_SpinBox, &opt, hint, this); 148 } 149 return cachedMinimumSizeHint; 150 } 151 152 private: 153 BitcoinUnit currentUnit{BitcoinUnit::BTC}; 154 CAmount singleStep{CAmount(100000)}; // satoshis 155 mutable QSize cachedMinimumSizeHint; 156 bool m_allow_empty{true}; 157 CAmount m_min_amount{CAmount(0)}; 158 CAmount m_max_amount{BitcoinUnits::maxMoney()}; 159 160 /** 161 * Parse a string into a number of base monetary units and 162 * return validity. 163 * @note Must return 0 if !valid. 164 */ 165 CAmount parse(const QString &text, bool *valid_out=nullptr) const 166 { 167 CAmount val = 0; 168 bool valid = BitcoinUnits::parse(currentUnit, text, &val); 169 if(valid) 170 { 171 if(val < 0 || val > BitcoinUnits::maxMoney()) 172 valid = false; 173 } 174 if(valid_out) 175 *valid_out = valid; 176 return valid ? val : 0; 177 } 178 179 protected: 180 bool event(QEvent *event) override 181 { 182 if (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease) 183 { 184 QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event); 185 if (keyEvent->key() == Qt::Key_Comma) 186 { 187 // Translate a comma into a period 188 QKeyEvent periodKeyEvent(event->type(), Qt::Key_Period, keyEvent->modifiers(), ".", keyEvent->isAutoRepeat(), keyEvent->count()); 189 return QAbstractSpinBox::event(&periodKeyEvent); 190 } 191 } 192 return QAbstractSpinBox::event(event); 193 } 194 195 StepEnabled stepEnabled() const override 196 { 197 if (isReadOnly()) // Disable steps when AmountSpinBox is read-only 198 return StepNone; 199 if (text().isEmpty()) // Allow step-up with empty field 200 return StepUpEnabled; 201 202 StepEnabled rv = StepNone; 203 bool valid = false; 204 CAmount val = value(&valid); 205 if (valid) { 206 if (val > m_min_amount) 207 rv |= StepDownEnabled; 208 if (val < m_max_amount) 209 rv |= StepUpEnabled; 210 } 211 return rv; 212 } 213 214 Q_SIGNALS: 215 void valueChanged(); 216 }; 217 218 #include <qt/bitcoinamountfield.moc> 219 220 BitcoinAmountField::BitcoinAmountField(QWidget* parent) 221 : QWidget(parent) 222 { 223 amount = new AmountSpinBox(this); 224 amount->setLocale(QLocale::c()); 225 amount->installEventFilter(this); 226 amount->setMaximumWidth(240); 227 228 QHBoxLayout *layout = new QHBoxLayout(this); 229 layout->addWidget(amount); 230 unit = new QValueComboBox(this); 231 unit->setModel(new BitcoinUnits(this)); 232 layout->addWidget(unit); 233 layout->addStretch(1); 234 layout->setContentsMargins(0,0,0,0); 235 236 setLayout(layout); 237 238 setFocusPolicy(Qt::TabFocus); 239 setFocusProxy(amount); 240 241 // If one if the widgets changes, the combined content changes as well 242 connect(amount, &AmountSpinBox::valueChanged, this, &BitcoinAmountField::valueChanged); 243 connect(unit, qOverload<int>(&QComboBox::currentIndexChanged), this, &BitcoinAmountField::unitChanged); 244 245 // Set default based on configuration 246 unitChanged(unit->currentIndex()); 247 } 248 249 void BitcoinAmountField::clear() 250 { 251 amount->clear(); 252 unit->setCurrentIndex(0); 253 } 254 255 void BitcoinAmountField::setEnabled(bool fEnabled) 256 { 257 amount->setEnabled(fEnabled); 258 unit->setEnabled(fEnabled); 259 } 260 261 bool BitcoinAmountField::validate() 262 { 263 bool valid = false; 264 value(&valid); 265 setValid(valid); 266 return valid; 267 } 268 269 void BitcoinAmountField::setValid(bool valid) 270 { 271 if (valid) 272 amount->setStyleSheet(""); 273 else 274 amount->setStyleSheet(STYLE_INVALID); 275 } 276 277 bool BitcoinAmountField::eventFilter(QObject *object, QEvent *event) 278 { 279 if (event->type() == QEvent::FocusIn) 280 { 281 // Clear invalid flag on focus 282 setValid(true); 283 } 284 return QWidget::eventFilter(object, event); 285 } 286 287 QWidget *BitcoinAmountField::setupTabChain(QWidget *prev) 288 { 289 QWidget::setTabOrder(prev, amount); 290 QWidget::setTabOrder(amount, unit); 291 return unit; 292 } 293 294 CAmount BitcoinAmountField::value(bool *valid_out) const 295 { 296 return amount->value(valid_out); 297 } 298 299 void BitcoinAmountField::setValue(const CAmount& value) 300 { 301 amount->setValue(value); 302 } 303 304 void BitcoinAmountField::SetAllowEmpty(bool allow) 305 { 306 amount->SetAllowEmpty(allow); 307 } 308 309 void BitcoinAmountField::SetMinValue(const CAmount& value) 310 { 311 amount->SetMinValue(value); 312 } 313 314 void BitcoinAmountField::SetMaxValue(const CAmount& value) 315 { 316 amount->SetMaxValue(value); 317 } 318 319 void BitcoinAmountField::setReadOnly(bool fReadOnly) 320 { 321 amount->setReadOnly(fReadOnly); 322 } 323 324 void BitcoinAmountField::unitChanged(int idx) 325 { 326 // Use description tooltip for current unit for the combobox 327 unit->setToolTip(unit->itemData(idx, Qt::ToolTipRole).toString()); 328 329 // Determine new unit ID 330 QVariant new_unit = unit->currentData(BitcoinUnits::UnitRole); 331 assert(new_unit.isValid()); 332 amount->setDisplayUnit(new_unit.value<BitcoinUnit>()); 333 } 334 335 void BitcoinAmountField::setDisplayUnit(BitcoinUnit new_unit) 336 { 337 unit->setValue(QVariant::fromValue(new_unit)); 338 } 339 340 void BitcoinAmountField::setSingleStep(const CAmount& step) 341 { 342 amount->setSingleStep(step); 343 }