File indexing completed on 2024-05-12 16:43:56
0001 /* 0002 SPDX-FileCopyrightText: 2010-2019 Thomas Baumgart <tbaumgart@kde.org> 0003 SPDX-FileCopyrightText: 2017-2018 Łukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com> 0004 SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 0007 #include "amountedit.h" 0008 0009 // ---------------------------------------------------------------------------- 0010 // QT Includes 0011 0012 #include <QApplication> 0013 #include <QDesktopWidget> 0014 #include <QKeyEvent> 0015 #include <QStyle> 0016 #include <QToolButton> 0017 #include <QFrame> 0018 #include <QLocale> 0019 0020 // ---------------------------------------------------------------------------- 0021 // KDE Includes 0022 0023 #include <KConfigGroup> 0024 #include <KSharedConfig> 0025 0026 // ---------------------------------------------------------------------------- 0027 // Project Includes 0028 0029 #include "amountvalidator.h" 0030 #include "kmymoneycalculator.h" 0031 #include "mymoneysecurity.h" 0032 #include "icons.h" 0033 #include "popuppositioner.h" 0034 0035 using namespace Icons; 0036 0037 class AmountEditHelper 0038 { 0039 public: 0040 AmountEditHelper() : q(nullptr) {} 0041 ~AmountEditHelper() { 0042 delete q; 0043 } 0044 AmountEdit *q; 0045 }; 0046 0047 Q_GLOBAL_STATIC(AmountEditHelper, s_globalAmountEdit) 0048 0049 AmountEdit* AmountEdit::global() 0050 { 0051 if (!s_globalAmountEdit()->q) { 0052 s_globalAmountEdit()->q = new AmountEdit(0, 2); 0053 } 0054 0055 return s_globalAmountEdit()->q; 0056 } 0057 0058 class AmountEditPrivate 0059 { 0060 Q_DISABLE_COPY(AmountEditPrivate) 0061 Q_DECLARE_PUBLIC(AmountEdit) 0062 0063 public: 0064 explicit AmountEditPrivate(AmountEdit* qq) : 0065 q_ptr(qq), 0066 m_calculatorFrame(nullptr), 0067 m_calculator(nullptr), 0068 m_calculatorButton(nullptr), 0069 m_prec(2), 0070 m_allowEmpty(false) 0071 { 0072 m_calculatorFrame = new QFrame; 0073 m_calculatorFrame->setWindowFlags(Qt::Popup); 0074 0075 m_calculatorFrame->setFrameStyle(QFrame::Panel | QFrame::Raised); 0076 m_calculatorFrame->setLineWidth(3); 0077 0078 m_calculator = new KMyMoneyCalculator(m_calculatorFrame); 0079 m_calculatorFrame->hide(); 0080 } 0081 0082 void init() 0083 { 0084 Q_Q(AmountEdit); 0085 // Yes, just a simple double validator ! 0086 auto validator = new AmountValidator(q); 0087 q->setValidator(validator); 0088 q->setAlignment(Qt::AlignRight | Qt::AlignVCenter); 0089 0090 int height = q->sizeHint().height(); 0091 int btnSize = q->sizeHint().height() - 5; 0092 0093 m_calculatorButton = new QToolButton(q); 0094 m_calculatorButton->setIcon(Icons::get(Icon::Calculator)); 0095 m_calculatorButton->setCursor(Qt::ArrowCursor); 0096 m_calculatorButton->setStyleSheet("QToolButton { border: none; padding: 2px}"); 0097 m_calculatorButton->setFixedSize(btnSize, btnSize); 0098 m_calculatorButton->setFocusPolicy(Qt::ClickFocus); 0099 m_calculatorButton->show(); 0100 0101 int frameWidth = q->style()->pixelMetric(QStyle::PM_DefaultFrameWidth); 0102 q->setStyleSheet(QString("QLineEdit { padding-right: %1px }") 0103 .arg(btnSize - frameWidth)); 0104 q->setMinimumHeight(height); 0105 0106 q->connect(m_calculatorButton, &QAbstractButton::clicked, q, &AmountEdit::slotCalculatorOpen); 0107 0108 KSharedConfig::Ptr kconfig = KSharedConfig::openConfig(); 0109 KConfigGroup grp = kconfig->group("General Options"); 0110 if (grp.readEntry("DontShowCalculatorButton", false) == true) 0111 q->setCalculatorButtonVisible(false); 0112 0113 q->connect(q, &QLineEdit::textChanged, q, &AmountEdit::theTextChanged); 0114 q->connect(m_calculator, &KMyMoneyCalculator::signalResultAvailable, q, &AmountEdit::slotCalculatorResult); 0115 q->connect(m_calculator, &KMyMoneyCalculator::signalQuit, q, &AmountEdit::slotCalculatorClose); 0116 } 0117 0118 /** 0119 * Internal helper function for value() and ensureFractionalPart(). 0120 */ 0121 void ensureFractionalPart(QString& s) const 0122 { 0123 s = MyMoneyMoney(s).formatMoney(QString(), m_prec, false); 0124 } 0125 0126 /** 0127 * This method opens the calculator and replays the key 0128 * event pointed to by @p ev. If @p ev is 0, then no key 0129 * event is replayed. 0130 * 0131 * @param ev pointer to QKeyEvent that started the calculator. 0132 */ 0133 void calculatorOpen(QKeyEvent* k) 0134 { 0135 Q_Q(AmountEdit); 0136 m_calculator->setInitialValues(q->text(), k); 0137 0138 // do not open the calculator in read-only mode 0139 if (q->isReadOnly()) 0140 return; 0141 0142 // show calculator and update size 0143 m_calculatorFrame->show(); 0144 m_calculatorFrame->setGeometry(m_calculator->geometry()); 0145 0146 PopupPositioner pos(q, m_calculatorFrame, PopupPositioner::BottemLeft); 0147 m_calculator->setFocus(); 0148 } 0149 0150 void cut() 0151 { 0152 Q_Q(AmountEdit); 0153 // only cut if parts of the text are selected 0154 if (q->hasSelectedText() && (q->text() != q->selectedText())) { 0155 cut(); 0156 } 0157 } 0158 0159 AmountEdit* q_ptr; 0160 QFrame* m_calculatorFrame; 0161 KMyMoneyCalculator* m_calculator; 0162 QToolButton* m_calculatorButton; 0163 int m_prec; 0164 bool m_allowEmpty; 0165 QString m_previousText; // keep track of what has been typed 0166 QString m_text; // keep track of what was the original value 0167 /** 0168 * This holds the number of precision to be used 0169 * when no other information (e.g. from account) 0170 * is available. 0171 * 0172 * @sa setStandardPrecision() 0173 */ 0174 }; 0175 0176 AmountEdit::AmountEdit(QWidget *parent, const int prec) : 0177 QLineEdit(parent), 0178 d_ptr(new AmountEditPrivate(this)) 0179 { 0180 Q_D(AmountEdit); 0181 d->m_prec = prec; 0182 if (prec < -1 || prec > 20) { 0183 d->m_prec = AmountEdit::global()->standardPrecision(); 0184 } 0185 d->init(); 0186 } 0187 0188 AmountEdit::AmountEdit(const MyMoneySecurity& sec, QWidget *parent) : 0189 QLineEdit(parent), 0190 d_ptr(new AmountEditPrivate(this)) 0191 { 0192 Q_D(AmountEdit); 0193 d->m_prec = MyMoneyMoney::denomToPrec(sec.smallestAccountFraction()); 0194 d->init(); 0195 } 0196 0197 AmountEdit::~AmountEdit() 0198 { 0199 Q_D(AmountEdit); 0200 delete d; 0201 } 0202 0203 void AmountEdit::setStandardPrecision(int prec) 0204 { 0205 if (prec >= 0 && prec < 20) { 0206 global()->d_ptr->m_prec = prec; 0207 } 0208 } 0209 0210 int AmountEdit::standardPrecision() 0211 { 0212 return global()->d_ptr->m_prec; 0213 } 0214 0215 0216 void AmountEdit::resizeEvent(QResizeEvent* event) 0217 { 0218 Q_D(AmountEdit); 0219 Q_UNUSED(event); 0220 const int frameWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth); 0221 d->m_calculatorButton->move(width() - d->m_calculatorButton->width() - frameWidth - 2, 2); 0222 } 0223 0224 void AmountEdit::focusInEvent(QFocusEvent* event) 0225 { 0226 QLineEdit::focusInEvent(event); 0227 if (event->reason() == Qt::MouseFocusReason) { 0228 if (!hasSelectedText()) { 0229 // we need to wait until all processing is done before 0230 // we can successfully call selectAll. Hence the 0231 // delayed execution when we return back to the event loop 0232 metaObject()->invokeMethod(this, &QLineEdit::selectAll, Qt::QueuedConnection); 0233 } 0234 } 0235 } 0236 0237 void AmountEdit::focusOutEvent(QFocusEvent* event) 0238 { 0239 Q_D(AmountEdit); 0240 QLineEdit::focusOutEvent(event); 0241 0242 // make sure we have a zero value in case the current text 0243 // is empty but this is not allowed 0244 if (text().isEmpty() && !d->m_allowEmpty) { 0245 QLineEdit::setText(QLatin1String("0")); 0246 } 0247 0248 // make sure we have a fractional part 0249 if (!text().isEmpty()) 0250 ensureFractionalPart(); 0251 0252 // in case the widget contains a different value we emit 0253 // the valueChanged signal 0254 if (MyMoneyMoney(text()) != MyMoneyMoney(d->m_text)) { 0255 emit valueChanged(text()); 0256 } 0257 } 0258 0259 void AmountEdit::keyPressEvent(QKeyEvent* event) 0260 { 0261 Q_D(AmountEdit); 0262 switch(event->key()) { 0263 case Qt::Key_Plus: 0264 case Qt::Key_Minus: 0265 d->cut(); 0266 if (text().length() == 0) { 0267 QLineEdit::keyPressEvent(event); 0268 break; 0269 } 0270 // in case of '-' we do not enter the calculator when 0271 // the current position is the beginning and there is 0272 // no '-' sign at the first position. 0273 if (event->key() == Qt::Key_Minus) { 0274 if (cursorPosition() == 0 && text()[0] != '-') { 0275 QLineEdit::keyPressEvent(event); 0276 break; 0277 } 0278 } 0279 // intentional fall through 0280 0281 case Qt::Key_Slash: 0282 case Qt::Key_Asterisk: 0283 case Qt::Key_Percent: 0284 d->cut(); 0285 d->calculatorOpen(event); 0286 break; 0287 0288 default: 0289 // make sure to use the locale's decimalPoint when the 0290 // keypad comma/dot is pressed 0291 auto keyText = event->text(); 0292 auto key = event->key(); 0293 if (event->modifiers() & Qt::KeypadModifier) { 0294 if ((key == Qt::Key_Period) || (key == Qt::Key_Comma)) { 0295 key = QLocale().decimalPoint().unicode(); 0296 keyText = QLocale().decimalPoint(); 0297 } 0298 } 0299 // create a (possibly adjusted) copy of the event 0300 QKeyEvent newEvent(event->type(), 0301 key, 0302 event->modifiers(), 0303 event->nativeScanCode(), 0304 event->nativeVirtualKey(), 0305 event->nativeModifiers(), 0306 keyText, 0307 event->isAutoRepeat(), 0308 event->count()); 0309 0310 // in case all text is selected and the user presses the decimal point 0311 // we fill the widget with the leading "0". The outcome of this will be 0312 // that the widget then contains "0.". 0313 if ((newEvent.key() == QLocale().decimalPoint()) && (selectedText() == text())) { 0314 QLineEdit::setText(QLatin1String("0")); 0315 } 0316 QLineEdit::keyPressEvent(&newEvent); 0317 break; 0318 } 0319 } 0320 0321 0322 void AmountEdit::setPrecision(const int prec) 0323 { 0324 Q_D(AmountEdit); 0325 if (prec >= -1 && prec <= 20) { 0326 if (prec != d->m_prec) { 0327 d->m_prec = prec; 0328 // update current display 0329 setValue(value()); 0330 } 0331 } 0332 } 0333 0334 int AmountEdit::precision() const 0335 { 0336 Q_D(const AmountEdit); 0337 return d->m_prec; 0338 } 0339 0340 bool AmountEdit::isValid() const 0341 { 0342 return !(text().isEmpty()); 0343 } 0344 0345 QString AmountEdit::numericalText() const 0346 { 0347 return value().toString(); 0348 } 0349 0350 MyMoneyMoney AmountEdit::value() const 0351 { 0352 Q_D(const AmountEdit); 0353 MyMoneyMoney money(text()); 0354 if (d->m_prec != -1) 0355 money = money.convert(MyMoneyMoney::precToDenom(d->m_prec)); 0356 return money; 0357 } 0358 0359 void AmountEdit::setValue(const MyMoneyMoney& value) 0360 { 0361 Q_D(AmountEdit); 0362 // load the value into the widget but don't use thousandsSeparators 0363 setText(value.formatMoney(QString(), d->m_prec, false)); 0364 } 0365 0366 void AmountEdit::setText(const QString& txt) 0367 { 0368 Q_D(AmountEdit); 0369 d->m_text = txt; 0370 if (isEnabled() && !txt.isEmpty()) 0371 d->ensureFractionalPart(d->m_text); 0372 QLineEdit::setText(d->m_text); 0373 #if 0 0374 m_resetButton->setEnabled(false); 0375 #endif 0376 } 0377 0378 void AmountEdit::resetText() 0379 { 0380 #if 0 0381 Q_D(AmountEdit); 0382 setText(d->m_text); 0383 m_resetButton->setEnabled(false); 0384 #endif 0385 } 0386 0387 void AmountEdit::theTextChanged(const QString & theText) 0388 { 0389 Q_D(AmountEdit); 0390 QLocale locale; 0391 QString dec = locale.groupSeparator(); 0392 QString l_text = theText; 0393 QString nsign, psign; 0394 nsign = locale.negativeSign(); 0395 psign = locale.positiveSign(); 0396 0397 auto i = 0; 0398 if (isEnabled()) { 0399 QValidator::State state = validator()->validate(l_text, i); 0400 if (state == QValidator::Intermediate) { 0401 if (l_text.length() == 1) { 0402 if (l_text != dec && l_text != nsign && l_text != psign) 0403 state = QValidator::Invalid; 0404 } 0405 } 0406 if (state == QValidator::Invalid) 0407 QLineEdit::setText(d->m_previousText); 0408 else { 0409 d->m_previousText = l_text; 0410 emit validatedTextChanged(text()); 0411 } 0412 } 0413 } 0414 0415 0416 void AmountEdit::slotCalculatorOpen() 0417 { 0418 Q_D(AmountEdit); 0419 d->calculatorOpen(0); 0420 } 0421 0422 void AmountEdit::slotCalculatorClose() 0423 { 0424 Q_D(AmountEdit); 0425 if (d->m_calculator != 0) { 0426 d->m_calculatorFrame->hide(); 0427 } 0428 } 0429 0430 void AmountEdit::slotCalculatorResult() 0431 { 0432 Q_D(AmountEdit); 0433 slotCalculatorClose(); 0434 if (d->m_calculator != 0) { 0435 setText(d->m_calculator->result()); 0436 ensureFractionalPart(); 0437 #if 0 0438 // I am not sure if getting a result from the calculator 0439 // is a good event to emit a value changed signal. We 0440 // should do this only on focusOutEvent() 0441 emit valueChanged(text()); 0442 d->m_text = text(); 0443 #endif 0444 } 0445 } 0446 0447 void AmountEdit::setCalculatorButtonVisible(const bool show) 0448 { 0449 Q_D(AmountEdit); 0450 d->m_calculatorButton->setVisible(show); 0451 } 0452 0453 void AmountEdit::setAllowEmpty(bool allowed) 0454 { 0455 Q_D(AmountEdit); 0456 d->m_allowEmpty = allowed; 0457 } 0458 0459 bool AmountEdit::isEmptyAllowed() const 0460 { 0461 Q_D(const AmountEdit); 0462 return d->m_allowEmpty; 0463 } 0464 0465 bool AmountEdit::isCalculatorButtonVisible() const 0466 { 0467 Q_D(const AmountEdit); 0468 return d->m_calculatorButton->isVisible(); 0469 } 0470 0471 void AmountEdit::ensureFractionalPart() 0472 { 0473 Q_D(AmountEdit); 0474 QString s(text()); 0475 d->ensureFractionalPart(s); 0476 // by setting the text only when it's different then the one that it is already there 0477 // we preserve the edit widget's state (like the selection for example) during a 0478 // call to ensureFractionalPart() that does not change anything 0479 if (s != text()) 0480 QLineEdit::setText(s); 0481 }