/ src / qt / bitcoinamountfield.cpp
bitcoinamountfield.cpp
  1  // Copyright (c) 2011-2022 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  }