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