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 }