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