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