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