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