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 }