File indexing completed on 2024-05-19 05:08:33

0001 /*
0002     SPDX-FileCopyrightText: 2002-2017 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 "kmymoneycalculator.h"
0008 
0009 // ----------------------------------------------------------------------------
0010 // QT Includes
0011 
0012 #include <QApplication>
0013 #include <QClipboard>
0014 #include <QFrame>
0015 #include <QGridLayout>
0016 #include <QKeyEvent>
0017 #include <QLabel>
0018 #include <QLocale>
0019 #include <QPushButton>
0020 #include <QRegularExpression>
0021 #include <QSignalMapper>
0022 
0023 // ----------------------------------------------------------------------------
0024 // KDE Includes
0025 
0026 // ----------------------------------------------------------------------------
0027 // Project Includes
0028 
0029 class KMyMoneyCalculatorPrivate
0030 {
0031     Q_DISABLE_COPY(KMyMoneyCalculatorPrivate)
0032 
0033 public:
0034     KMyMoneyCalculatorPrivate() :
0035         op0(0.0),
0036         op1(0.0),
0037         op(0),
0038         stackedOp(0),
0039         display(nullptr),
0040         m_clearOperandOnDigit(false)
0041     {
0042         for (auto& button : buttons) {
0043             button = nullptr;
0044         }
0045     }
0046 
0047     void updateOperand()
0048     {
0049         if (operand.length() > 16) {
0050             operand = operand.left(16);
0051         }
0052         changeDisplay(operand);
0053     }
0054 
0055     const QRegularExpression& commaRegex() const
0056     {
0057         static const QRegularExpression regex(QLatin1String("\\."));
0058         return regex;
0059     }
0060     void changeDisplay(const QString& str)
0061     {
0062         auto txt = str;
0063         txt.replace(commaRegex(), m_comma);
0064         display->setText("<b>" + txt + "</b>");
0065     }
0066 
0067     /**
0068       * This member variable stores the current (second) operand
0069       */
0070     QString operand;
0071 
0072     /**
0073       * This member variable stores the last result
0074       */
0075     QString m_result;
0076 
0077     /**
0078       * This member variable stores the representation of the
0079       * character to be used to separate the integer and fractional
0080       * part of numbers. The internal representation is always a period.
0081       */
0082     QString m_comma;
0083 
0084     /**
0085       * The numeric representation of a stacked first operand
0086       */
0087     double op0;
0088 
0089     /**
0090       * The numeric representation of the first operand
0091       */
0092     double op1;
0093 
0094     /**
0095       * This member stores the operation to be performed between
0096       * the first and the second operand.
0097       */
0098     int op;
0099 
0100     /**
0101      * This member stores a pending addition operation
0102      */
0103     int stackedOp;
0104 
0105     /**
0106       * This member stores a pointer to the display area
0107       */
0108     QLabel *display;
0109 
0110     /**
0111       * This member array stores the pointers to the various
0112       * buttons of the calculator. It is setup during the
0113       * constructor of this object
0114       */
0115     QPushButton *buttons[20];
0116 
0117     /**
0118       * This enumeration type stores the values used for the
0119       * various keys internally
0120       */
0121     enum {
0122         /* 0-9 are used by digits */
0123         COMMA = 10,
0124         /*
0125          * make sure, that PLUS through EQUAL remain in
0126          * the order they are. Otherwise, check the calculation
0127          * signal mapper
0128          */
0129         PLUS,
0130         MINUS,
0131         SLASH,
0132         STAR,
0133         EQUAL,
0134         PLUSMINUS,
0135         PERCENT,
0136         CLEAR,
0137         CLEARALL,
0138         /* insert new buttons before this line */
0139         MAX_BUTTONS,
0140     };
0141 
0142     /**
0143       * This flag signals, if the operand should be replaced upon
0144       * a digit key pressure. Defaults to false and will be set, if
0145       * setInitialValues() is called without an operation.
0146       */
0147     bool m_clearOperandOnDigit;
0148 };
0149 
0150 KMyMoneyCalculator::KMyMoneyCalculator(QWidget* parent) :
0151     QFrame(parent),
0152     d_ptr(new KMyMoneyCalculatorPrivate)
0153 {
0154     Q_D(KMyMoneyCalculator);
0155     d->m_comma = QLocale().decimalPoint();
0156     d->m_clearOperandOnDigit = false;
0157 
0158     QGridLayout* grid = new QGridLayout(this);
0159 
0160     d->display = new QLabel(this);
0161     QPalette palette;
0162     palette.setColor(d->display->backgroundRole(), QColor(0xBD, 0xFF, 0xB4));
0163     d->display->setPalette(palette);
0164 
0165     d->display->setFrameStyle(QFrame::Panel | QFrame::Sunken);
0166     d->display->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
0167     grid->addWidget(d->display, 0, 0, 1, 5);
0168 
0169     d->buttons[0] = new QPushButton("0", this);
0170     d->buttons[1] = new QPushButton("1", this);
0171     d->buttons[2] = new QPushButton("2", this);
0172     d->buttons[3] = new QPushButton("3", this);
0173     d->buttons[4] = new QPushButton("4", this);
0174     d->buttons[5] = new QPushButton("5", this);
0175     d->buttons[6] = new QPushButton("6", this);
0176     d->buttons[7] = new QPushButton("7", this);
0177     d->buttons[8] = new QPushButton("8", this);
0178     d->buttons[9] = new QPushButton("9", this);
0179     d->buttons[KMyMoneyCalculatorPrivate::PLUS] = new QPushButton("+", this);
0180     d->buttons[KMyMoneyCalculatorPrivate::MINUS] = new QPushButton("-", this);
0181     d->buttons[KMyMoneyCalculatorPrivate::STAR] = new QPushButton("X", this);
0182     d->buttons[KMyMoneyCalculatorPrivate::COMMA] = new QPushButton(d->m_comma, this);
0183     d->buttons[KMyMoneyCalculatorPrivate::EQUAL] = new QPushButton("=", this);
0184     d->buttons[KMyMoneyCalculatorPrivate::SLASH] = new QPushButton("/", this);
0185     d->buttons[KMyMoneyCalculatorPrivate::CLEAR] = new QPushButton("C", this);
0186     d->buttons[KMyMoneyCalculatorPrivate::CLEARALL] = new QPushButton("AC", this);
0187     d->buttons[KMyMoneyCalculatorPrivate::PLUSMINUS] = new QPushButton("+-", this);
0188     d->buttons[KMyMoneyCalculatorPrivate::PERCENT] = new QPushButton("%", this);
0189 
0190     grid->addWidget(d->buttons[7], 1, 0);
0191     grid->addWidget(d->buttons[8], 1, 1);
0192     grid->addWidget(d->buttons[9], 1, 2);
0193     grid->addWidget(d->buttons[4], 2, 0);
0194     grid->addWidget(d->buttons[5], 2, 1);
0195     grid->addWidget(d->buttons[6], 2, 2);
0196     grid->addWidget(d->buttons[1], 3, 0);
0197     grid->addWidget(d->buttons[2], 3, 1);
0198     grid->addWidget(d->buttons[3], 3, 2);
0199     grid->addWidget(d->buttons[0], 4, 1);
0200 
0201     grid->addWidget(d->buttons[KMyMoneyCalculatorPrivate::COMMA], 4, 0);
0202     grid->addWidget(d->buttons[KMyMoneyCalculatorPrivate::PLUS], 3, 3);
0203     grid->addWidget(d->buttons[KMyMoneyCalculatorPrivate::MINUS], 4, 3);
0204     grid->addWidget(d->buttons[KMyMoneyCalculatorPrivate::STAR], 3, 4);
0205     grid->addWidget(d->buttons[KMyMoneyCalculatorPrivate::SLASH], 4, 4);
0206     grid->addWidget(d->buttons[KMyMoneyCalculatorPrivate::EQUAL], 4, 2);
0207     grid->addWidget(d->buttons[KMyMoneyCalculatorPrivate::PLUSMINUS], 2, 3);
0208     grid->addWidget(d->buttons[KMyMoneyCalculatorPrivate::PERCENT], 2, 4);
0209     grid->addWidget(d->buttons[KMyMoneyCalculatorPrivate::CLEAR], 1, 3);
0210     grid->addWidget(d->buttons[KMyMoneyCalculatorPrivate::CLEARALL], 1, 4);
0211 
0212     d->buttons[KMyMoneyCalculatorPrivate::EQUAL]->setFocus();
0213 
0214     d->op1 = d->op0 = 0.0;
0215     d->stackedOp = d->op = 0;
0216     d->operand.clear();
0217     d->changeDisplay("0");
0218 
0219     // connect the digit signals through a signal mapper
0220     QSignalMapper* mapper = new QSignalMapper(this);
0221     for (auto i = 0; i < 10; ++i) {
0222         mapper->setMapping(d->buttons[i], i);
0223         connect(d->buttons[i], &QAbstractButton::clicked, mapper, static_cast<void (QSignalMapper::*)()>(&QSignalMapper::map));
0224     }
0225     connect(mapper, &QSignalMapper::mappedInt, this, &KMyMoneyCalculator::digitClicked);
0226 
0227     // connect the calculation operations through another mapper
0228     mapper = new QSignalMapper(this);
0229     for (int i = KMyMoneyCalculatorPrivate::PLUS; i <= KMyMoneyCalculatorPrivate::EQUAL; ++i) {
0230         mapper->setMapping(d->buttons[i], i);
0231         connect(d->buttons[i], &QAbstractButton::clicked, mapper, static_cast<void (QSignalMapper::*)()>(&QSignalMapper::map));
0232     }
0233     connect(mapper, &QSignalMapper::mappedInt, this, &KMyMoneyCalculator::calculationClicked);
0234 
0235     // connect all remaining signals
0236     connect(d->buttons[KMyMoneyCalculatorPrivate::COMMA], &QAbstractButton::clicked, this, &KMyMoneyCalculator::commaClicked);
0237     connect(d->buttons[KMyMoneyCalculatorPrivate::PLUSMINUS], &QAbstractButton::clicked, this, &KMyMoneyCalculator::plusminusClicked);
0238     connect(d->buttons[KMyMoneyCalculatorPrivate::PERCENT], &QAbstractButton::clicked, this, &KMyMoneyCalculator::percentClicked);
0239     connect(d->buttons[KMyMoneyCalculatorPrivate::CLEAR], &QAbstractButton::clicked, this, &KMyMoneyCalculator::clearClicked);
0240     connect(d->buttons[KMyMoneyCalculatorPrivate::CLEARALL], &QAbstractButton::clicked, this, &KMyMoneyCalculator::clearAllClicked);
0241 
0242     for (auto i = 0; i < KMyMoneyCalculatorPrivate::MAX_BUTTONS; ++i) {
0243         d->buttons[i]->setMinimumSize(40, 30);
0244         d->buttons[i]->setMaximumSize(40, 30);
0245     }
0246     // keep the size determined by the size of the contained buttons no matter what
0247     setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
0248 }
0249 
0250 KMyMoneyCalculator::~KMyMoneyCalculator()
0251 {
0252     Q_D(KMyMoneyCalculator);
0253     delete d;
0254 }
0255 
0256 void KMyMoneyCalculator::digitClicked(int button)
0257 {
0258     Q_D(KMyMoneyCalculator);
0259     if (d->m_clearOperandOnDigit) {
0260         d->operand.clear();
0261         d->m_clearOperandOnDigit = false;
0262     }
0263 
0264     d->operand += QChar(button + 0x30);
0265     d->updateOperand();
0266 }
0267 
0268 void KMyMoneyCalculator::commaClicked()
0269 {
0270     Q_D(KMyMoneyCalculator);
0271     if (d->operand.length() == 0) {
0272         d->operand = '0';
0273     }
0274     if (d->operand.contains('.', Qt::CaseInsensitive) == 0)
0275         d->operand.append('.');
0276 
0277     d->updateOperand();
0278 }
0279 
0280 void KMyMoneyCalculator::plusminusClicked()
0281 {
0282     Q_D(KMyMoneyCalculator);
0283     if (d->operand.length() == 0 && d->m_result.length() > 0) {
0284         d->operand = d->m_result;
0285     }
0286 
0287     if (d->operand.length() > 0) {
0288         if (d->operand.indexOf('-') != -1)
0289             d->operand.remove('-');
0290         else
0291             d->operand.prepend('-');
0292         d->changeDisplay(d->operand);
0293     }
0294 }
0295 
0296 void KMyMoneyCalculator::calculationClicked(int button)
0297 {
0298     Q_D(KMyMoneyCalculator);
0299     if (d->operand.length() == 0 && d->op != 0 && button == KMyMoneyCalculatorPrivate::EQUAL) {
0300         d->op = 0;
0301         d->m_result = normalizeString(d->op1);
0302         d->changeDisplay(d->m_result);
0303 
0304     } else if (d->operand.length() > 0 && d->op != 0) {
0305         // perform operation
0306         double op2 = d->operand.toDouble();
0307         bool error = false;
0308 
0309         // if the pending operation is addition and we now do multiplication
0310         // we just stack op1 and remember the operation in
0311         if ((d->op == KMyMoneyCalculatorPrivate::PLUS || d->op == KMyMoneyCalculatorPrivate::MINUS) && (button == KMyMoneyCalculatorPrivate::STAR || button == KMyMoneyCalculatorPrivate::SLASH)) {
0312             d->op0 = d->op1;
0313             d->stackedOp = d->op;
0314             d->op = 0;
0315         }
0316 
0317         switch (d->op) {
0318         case KMyMoneyCalculatorPrivate::PLUS:
0319             op2 = d->op1 + op2;
0320             break;
0321         case KMyMoneyCalculatorPrivate::MINUS:
0322             op2 = d->op1 - op2;
0323             break;
0324         case KMyMoneyCalculatorPrivate::STAR:
0325             op2 = d->op1 * op2;
0326             break;
0327         case KMyMoneyCalculatorPrivate::SLASH:
0328             if (op2 == 0.0)
0329                 error = true;
0330             else
0331                 op2 = d->op1 / op2;
0332             break;
0333         }
0334 
0335         // if we have a pending addition operation, and the next operation is
0336         // not multiplication, we calculate the stacked operation
0337         if (d->stackedOp && button != KMyMoneyCalculatorPrivate::STAR && button != KMyMoneyCalculatorPrivate::SLASH) {
0338             switch (d->stackedOp) {
0339             case KMyMoneyCalculatorPrivate::PLUS:
0340                 op2 = d->op0 + op2;
0341                 break;
0342             case KMyMoneyCalculatorPrivate::MINUS:
0343                 op2 = d->op0 - op2;
0344                 break;
0345             }
0346             d->stackedOp = 0;
0347         }
0348 
0349         if (error) {
0350             d->op = 0;
0351             d->changeDisplay("Error");
0352             d->operand.clear();
0353         } else {
0354             d->op1 = op2;
0355             d->m_result = normalizeString(d->op1);
0356             d->changeDisplay(d->m_result);
0357         }
0358     } else if (d->operand.length() > 0 && d->op == 0) {
0359         d->op1 = d->operand.toDouble();
0360         d->m_result = normalizeString(d->op1);
0361         d->changeDisplay(d->m_result);
0362     }
0363 
0364     if (button != KMyMoneyCalculatorPrivate::EQUAL) {
0365         d->op = button;
0366     } else {
0367         d->op = 0;
0368         Q_EMIT signalResultAvailable();
0369     }
0370     d->operand.clear();
0371 }
0372 
0373 QString KMyMoneyCalculator::normalizeString(const double& val)
0374 {
0375     QString str;
0376     str.setNum(val, 'f');
0377     int i = str.length();
0378     while (i > 1 && str[i-1] == '0') {
0379         --i;
0380     }
0381     // cut off trailing 0's
0382     str.remove(i, str.length());
0383     if (str.length() > 0) {
0384         // possibly remove trailing period
0385         if (str[str.length()-1] == '.') {
0386             str.remove(str.length() - 1, 1);
0387         }
0388     }
0389     return str;
0390 }
0391 
0392 void KMyMoneyCalculator::clearClicked()
0393 {
0394     Q_D(KMyMoneyCalculator);
0395     if (d->operand.length() > 0) {
0396         d->operand = d->operand.left(d->operand.length() - 1);
0397     }
0398     if (d->operand.length() == 0)
0399         d->changeDisplay("0");
0400     else
0401         d->changeDisplay(d->operand);
0402 }
0403 
0404 void KMyMoneyCalculator::clearAllClicked()
0405 {
0406     Q_D(KMyMoneyCalculator);
0407     d->operand.clear();
0408     d->op = 0;
0409     d->changeDisplay("0");
0410 }
0411 
0412 void KMyMoneyCalculator::percentClicked()
0413 {
0414     Q_D(KMyMoneyCalculator);
0415     if (d->op != 0) {
0416         double op2 = d->operand.toDouble();
0417         switch (d->op) {
0418         case KMyMoneyCalculatorPrivate::PLUS:
0419         case KMyMoneyCalculatorPrivate::MINUS:
0420             op2 = (d->op1 * op2) / 100;
0421             break;
0422 
0423         case KMyMoneyCalculatorPrivate::STAR:
0424         case KMyMoneyCalculatorPrivate::SLASH:
0425             op2 /= 100;
0426             break;
0427         }
0428         d->operand = normalizeString(op2);
0429         d->changeDisplay(d->operand);
0430     }
0431 }
0432 
0433 QString KMyMoneyCalculator::result() const
0434 {
0435     Q_D(const KMyMoneyCalculator);
0436     auto txt = d->m_result;
0437     txt.replace(d->commaRegex(), d->m_comma);
0438     if (txt[0] == '-') {
0439         txt = txt.mid(1); // get rid of the minus sign
0440         QString mask;
0441         // TODO: port this to kf5 (support for paren around negative numbers)
0442 #if 0
0443         switch (KLocale::global()->negativeMonetarySignPosition()) {
0444         case KLocale::ParensAround:
0445             mask = "(%1)";
0446             break;
0447         case KLocale::AfterQuantityMoney:
0448             mask = "%1-";
0449             break;
0450         case KLocale::AfterMoney:
0451         case KLocale::BeforeMoney:
0452             mask = "%1 -";
0453             break;
0454         case KLocale::BeforeQuantityMoney:
0455             mask = "-%1";
0456             break;
0457         }
0458 #else
0459         mask = "-%1";
0460 #endif
0461         txt = QString(mask).arg(txt);
0462     }
0463     return txt;
0464 }
0465 
0466 void KMyMoneyCalculator::setComma(const QChar ch)
0467 {
0468     Q_D(KMyMoneyCalculator);
0469     d->m_comma = ch;
0470 }
0471 
0472 void KMyMoneyCalculator::keyPressEvent(QKeyEvent* ev)
0473 {
0474     Q_D(KMyMoneyCalculator);
0475     int button = -1;
0476 
0477     if (ev->matches(QKeySequence::Paste)) {
0478         const auto clipboard = qApp->clipboard();
0479         auto txt = clipboard->text();
0480 
0481         // check that the clipboard content is valid
0482         bool ok;
0483         QLocale().toDouble(txt, &ok);
0484 
0485         // Now convert the localized command into a dot
0486         txt.replace(d->m_comma, QLatin1String("."));
0487 
0488         // since toDouble() allows scientific notation, we
0489         // make sure the letter e is not contained
0490         if (ok && !(txt.contains(QLatin1Char('e'), Qt::CaseInsensitive))) {
0491             if (d->m_clearOperandOnDigit) {
0492                 d->operand.clear();
0493                 d->m_clearOperandOnDigit = false;
0494             }
0495             d->operand += txt;
0496             d->updateOperand();
0497         }
0498         return;
0499     }
0500 
0501     switch (ev->key()) {
0502     case Qt::Key_0:
0503     case Qt::Key_1:
0504     case Qt::Key_2:
0505     case Qt::Key_3:
0506     case Qt::Key_4:
0507     case Qt::Key_5:
0508     case Qt::Key_6:
0509     case Qt::Key_7:
0510     case Qt::Key_8:
0511     case Qt::Key_9:
0512         if (d->m_clearOperandOnDigit) {
0513             d->operand.clear();
0514             d->m_clearOperandOnDigit = false;
0515         }
0516         button = ev->key() - Qt::Key_0;
0517         break;
0518     case Qt::Key_Plus:
0519         button = KMyMoneyCalculatorPrivate::PLUS;
0520         break;
0521     case Qt::Key_Minus:
0522         button = KMyMoneyCalculatorPrivate::MINUS;
0523         break;
0524     case Qt::Key_Comma:
0525     case Qt::Key_Period:
0526         if (d->m_clearOperandOnDigit) {
0527             d->operand.clear();
0528             d->m_clearOperandOnDigit = false;
0529         }
0530         button = KMyMoneyCalculatorPrivate::COMMA;
0531         break;
0532     case Qt::Key_Slash:
0533         button = KMyMoneyCalculatorPrivate::SLASH;
0534         break;
0535     case Qt::Key_Backspace:
0536         button = KMyMoneyCalculatorPrivate::CLEAR;
0537         if(ev->modifiers() & Qt::ShiftModifier) {
0538             button = KMyMoneyCalculatorPrivate::CLEARALL;
0539         }
0540         break;
0541     case Qt::Key_Asterisk:
0542         button = KMyMoneyCalculatorPrivate::STAR;
0543         break;
0544     case Qt::Key_Return:
0545     case Qt::Key_Enter:
0546     case Qt::Key_Equal:
0547         button = KMyMoneyCalculatorPrivate::EQUAL;
0548         break;
0549     case Qt::Key_Escape:
0550         Q_EMIT signalQuit();
0551         break;
0552     case Qt::Key_Percent:
0553         button = KMyMoneyCalculatorPrivate::PERCENT;
0554         break;
0555     default:
0556         ev->ignore();
0557         break;
0558     }
0559     if (button != -1)
0560         d->buttons[button]->animateClick();
0561 
0562     d->m_clearOperandOnDigit = false;
0563 }
0564 
0565 void KMyMoneyCalculator::setInitialValues(const QString& value, QKeyEvent* ev)
0566 {
0567     Q_D(KMyMoneyCalculator);
0568     bool negative = false;
0569     // setup operand
0570     d->operand = value;
0571     // make sure the group/thousands separator is removed ...
0572     d->operand.replace(QRegularExpression(QStringLiteral("\\%1").arg(QLocale().groupSeparator())), QChar());
0573     // ... and the decimal is represented by a dot
0574     d->operand.replace(QRegularExpression(QStringLiteral("\\%1").arg(d->m_comma)), QChar('.'));
0575     if (d->operand.contains('(')) {
0576         negative = true;
0577         d->operand.remove('(');
0578         d->operand.remove(')');
0579     }
0580     if (d->operand.contains('-')) {
0581         negative = true;
0582         d->operand.remove('-');
0583     }
0584     if (d->operand.isEmpty()) {
0585         d->operand.clear();
0586         d->changeDisplay(QLatin1String("0"));
0587     } else {
0588         if (negative) {
0589             d->operand = QStringLiteral("-%1").arg(d->operand);
0590         }
0591         d->changeDisplay(d->operand);
0592     }
0593 
0594     // and operation
0595     d->op = 0;
0596     if (ev)
0597         keyPressEvent(ev);
0598     else
0599         d->m_clearOperandOnDigit = true;
0600 }