limenkaamountfield.cpp raw

   1  // Copyright (c) 2011-2022 The Limenka 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/limenkaamountfield.h>
   6  
   7  #include <qt/limenkaunits.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 = LimenkaUnits::format(currentUnit, val, false, LimenkaUnits::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(LimenkaUnits::format(currentUnit, value, false, LimenkaUnits::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          CAmount currentSingleStep = singleStep;
  98          if (!currentSingleStep) {
  99              currentSingleStep = LimenkaUnits::singlestep(currentUnit);
 100          }
 101          val = val + steps * currentSingleStep;
 102          val = qBound(m_min_amount, val, m_max_amount);
 103          setValue(val);
 104      }
 105  
 106      void setDisplayUnit(LimenkaUnit unit)
 107      {
 108          bool valid = false;
 109          CAmount val = value(&valid);
 110  
 111          currentUnit = unit;
 112          lineEdit()->setPlaceholderText(LimenkaUnits::format(currentUnit, m_min_amount, false, LimenkaUnits::SeparatorStyle::ALWAYS));
 113          if(valid)
 114              setValue(val);
 115          else
 116              clear();
 117      }
 118  
 119      void setFont(const QFont& font)
 120      {
 121          lineEdit()->setFont(font);
 122      }
 123  
 124      void setSingleStep(const CAmount& step)
 125      {
 126          singleStep = step;
 127      }
 128  
 129      QSize minimumSizeHint() const override
 130      {
 131          if(cachedMinimumSizeHint.isEmpty())
 132          {
 133              ensurePolished();
 134  
 135              const QFontMetrics fm(fontMetrics());
 136              int h = lineEdit()->minimumSizeHint().height();
 137              int w = GUIUtil::TextWidth(fm, LimenkaUnits::format(LimenkaUnit::BTC, LimenkaUnits::maxMoney(), false, LimenkaUnits::SeparatorStyle::ALWAYS));
 138              w += 2; // cursor blinking space
 139  
 140              QStyleOptionSpinBox opt;
 141              initStyleOption(&opt);
 142              QSize hint(w, h);
 143              QSize extra(35, 6);
 144              opt.rect.setSize(hint + extra);
 145              extra += hint - style()->subControlRect(QStyle::CC_SpinBox, &opt,
 146                                                      QStyle::SC_SpinBoxEditField, this).size();
 147              // get closer to final result by repeating the calculation
 148              opt.rect.setSize(hint + extra);
 149              extra += hint - style()->subControlRect(QStyle::CC_SpinBox, &opt,
 150                                                      QStyle::SC_SpinBoxEditField, this).size();
 151              hint += extra;
 152              hint.setHeight(h);
 153  
 154              opt.rect = rect();
 155  
 156              cachedMinimumSizeHint = style()->sizeFromContents(QStyle::CT_SpinBox, &opt, hint, this);
 157          }
 158          return cachedMinimumSizeHint;
 159      }
 160  
 161  private:
 162      LimenkaUnit currentUnit{LimenkaUnit::BTC};
 163      CAmount singleStep{CAmount(0)};
 164      mutable QSize cachedMinimumSizeHint;
 165      bool m_allow_empty{true};
 166      CAmount m_min_amount{CAmount(0)};
 167      CAmount m_max_amount{LimenkaUnits::maxMoney()};
 168  
 169      /**
 170       * Parse a string into a number of base monetary units and
 171       * return validity.
 172       * @note Must return 0 if !valid.
 173       */
 174      CAmount parse(const QString &text, bool *valid_out=nullptr) const
 175      {
 176          CAmount val = 0;
 177          bool valid = LimenkaUnits::parse(currentUnit, text, &val);
 178          if(valid)
 179          {
 180              if(val < 0 || val > LimenkaUnits::maxMoney())
 181                  valid = false;
 182          }
 183          if(valid_out)
 184              *valid_out = valid;
 185          return valid ? val : 0;
 186      }
 187  
 188  protected:
 189      bool event(QEvent *event) override
 190      {
 191          if (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease)
 192          {
 193              QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
 194              if (keyEvent->key() == Qt::Key_Comma)
 195              {
 196                  // Translate a comma into a period
 197                  QKeyEvent periodKeyEvent(event->type(), Qt::Key_Period, keyEvent->modifiers(), ".", keyEvent->isAutoRepeat(), keyEvent->count());
 198                  return QAbstractSpinBox::event(&periodKeyEvent);
 199              }
 200          }
 201          return QAbstractSpinBox::event(event);
 202      }
 203  
 204      StepEnabled stepEnabled() const override
 205      {
 206          if (isReadOnly()) // Disable steps when AmountSpinBox is read-only
 207              return StepNone;
 208          if (text().isEmpty()) // Allow step-up with empty field
 209              return StepUpEnabled;
 210  
 211          StepEnabled rv = StepNone;
 212          bool valid = false;
 213          CAmount val = value(&valid);
 214          if (valid) {
 215              if (val > m_min_amount)
 216                  rv |= StepDownEnabled;
 217              if (val < m_max_amount)
 218                  rv |= StepUpEnabled;
 219          }
 220          return rv;
 221      }
 222  
 223  Q_SIGNALS:
 224      void valueChanged();
 225  };
 226  
 227  #include <qt/limenkaamountfield.moc>
 228  
 229  LimenkaAmountField::LimenkaAmountField(QWidget* parent)
 230      : QWidget(parent)
 231  {
 232      amount = new AmountSpinBox(this);
 233      amount->setLocale(QLocale::c());
 234      amount->installEventFilter(this);
 235      amount->setMaximumWidth(240);
 236  
 237      QHBoxLayout *layout = new QHBoxLayout(this);
 238      layout->addWidget(amount);
 239      unit = new QValueComboBox(this);
 240      unit->setModel(new LimenkaUnits(this));
 241      layout->addWidget(unit);
 242      layout->addStretch(1);
 243      layout->setContentsMargins(0,0,0,0);
 244  
 245      setLayout(layout);
 246  
 247      setFocusPolicy(Qt::TabFocus);
 248      setFocusProxy(amount);
 249  
 250      // If one if the widgets changes, the combined content changes as well
 251      connect(amount, &AmountSpinBox::valueChanged, this, &LimenkaAmountField::valueChanged);
 252      connect(unit, qOverload<int>(&QComboBox::currentIndexChanged), this, &LimenkaAmountField::unitChanged);
 253  
 254      // Set default based on configuration
 255      unitChanged(unit->currentIndex());
 256  }
 257  
 258  void LimenkaAmountField::clear()
 259  {
 260      amount->clear();
 261      unit->setCurrentIndex(0);
 262  }
 263  
 264  void LimenkaAmountField::setEnabled(bool fEnabled)
 265  {
 266      amount->setEnabled(fEnabled);
 267      unit->setEnabled(fEnabled);
 268  }
 269  
 270  bool LimenkaAmountField::validate()
 271  {
 272      bool valid = false;
 273      value(&valid);
 274      setValid(valid);
 275      return valid;
 276  }
 277  
 278  void LimenkaAmountField::setValid(bool valid)
 279  {
 280      const QString style = valid ? QString() : QStringLiteral(STYLE_INVALID);
 281      if (amount->styleSheet() != style) {
 282          // CAUTION: Some Qt styles (Breeze in particular) add event handlers in setStyleSheet, which causes the eventFilter to run infinitely; use a QueuedConnection to change it outside of the eventFilter instead
 283          QMetaObject::invokeMethod(amount, "setStyleSheet", Qt::QueuedConnection, Q_ARG(QString, style));
 284      }
 285  }
 286  
 287  bool LimenkaAmountField::eventFilter(QObject *object, QEvent *event)
 288  {
 289      if (event->type() == QEvent::FocusIn)
 290      {
 291          // Clear invalid flag on focus
 292          setValid(true);
 293      }
 294      return QWidget::eventFilter(object, event);
 295  }
 296  
 297  QWidget *LimenkaAmountField::setupTabChain(QWidget *prev)
 298  {
 299      QWidget::setTabOrder(prev, amount);
 300      QWidget::setTabOrder(amount, unit);
 301      return unit;
 302  }
 303  
 304  CAmount LimenkaAmountField::value(bool *valid_out) const
 305  {
 306      return amount->value(valid_out);
 307  }
 308  
 309  void LimenkaAmountField::setValue(const CAmount& value)
 310  {
 311      amount->setValue(value);
 312  }
 313  
 314  void LimenkaAmountField::SetAllowEmpty(bool allow)
 315  {
 316      amount->SetAllowEmpty(allow);
 317  }
 318  
 319  void LimenkaAmountField::SetMinValue(const CAmount& value)
 320  {
 321      amount->SetMinValue(value);
 322  }
 323  
 324  void LimenkaAmountField::SetMaxValue(const CAmount& value)
 325  {
 326      amount->SetMaxValue(value);
 327  }
 328  
 329  void LimenkaAmountField::setReadOnly(bool fReadOnly)
 330  {
 331      amount->setReadOnly(fReadOnly);
 332  }
 333  
 334  void LimenkaAmountField::unitChanged(int idx)
 335  {
 336      // Use description tooltip for current unit for the combobox
 337      unit->setToolTip(unit->itemData(idx, Qt::ToolTipRole).toString());
 338  
 339      // Determine new unit ID
 340      QVariant new_unit = unit->currentData(LimenkaUnits::UnitRole);
 341      assert(new_unit.isValid());
 342      amount->setDisplayUnit(new_unit.value<LimenkaUnit>());
 343  }
 344  
 345  void LimenkaAmountField::setDisplayUnit(LimenkaUnit new_unit)
 346  {
 347      unit->setValue(QVariant::fromValue(new_unit));
 348  }
 349  
 350  void LimenkaAmountField::setFontForMoney(const QFont& font_for_money)
 351  {
 352      amount->setFont(font_for_money);
 353  }
 354  
 355  void LimenkaAmountField::setSingleStep(const CAmount& step)
 356  {
 357      amount->setSingleStep(step);
 358  }
 359