File indexing completed on 2024-05-12 16:44:01

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