bitcoinamountfield.cpp raw

   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  }
 344