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