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 }