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