/* * SPDX-FileCopyrightText: 2020-2021 Han Young * SPDX-FileCopyrightText: 2021-2022 Rohan Asokan * * SPDX-License-Identifier: GPL-3.0-or-later */ #include "inputmanager.h" #include "historymanager.h" #include "qalculateengine.h" #include #include constexpr QStringView ZERO_WIDTH_SPACE = u"\u200B"; InputManager::InputManager(QObject *parent) : QObject(parent) , m_engine(QalculateEngine::inst()) { QLocale locale; m_groupSeparator = locale.groupSeparator(); // change non breaking space into classic space, because UI converts it to classic space if (m_groupSeparator == QString(QChar(160))) { m_groupSeparator = QLatin1Char(32); } m_decimalPoint = locale.decimalPoint(); } InputManager::~InputManager() = default; const QString &InputManager::expression() const { return m_expression; } const QString &InputManager::result() const { return m_result; } const QString &InputManager::binaryResult() const { return m_binaryResult; } const QString &InputManager::hexResult() const { return m_hexResult; } bool InputManager::moveFromResult() const { return m_moveFromResult; } int InputManager::getCursorPosition() const { int position = m_inputPosition; // account for group separators in expression int i = 0; while (i < position && i < m_expression.size()) { if (m_expression.at(i) == m_groupSeparator) { position++; } i++; } return position; } void InputManager::setCursorPosition(int position) { m_inputPosition = position; int i = 0; while (i < position && i < m_expression.size()) { if (m_expression.at(i) == m_groupSeparator) { m_inputPosition--; } i++; } } int InputManager::idealCursorPosition(int position, int arrow) const { // position cursor ahead of group separator if (position > 1 && position < m_expression.size()) { if (m_expression.at(position - 1) == m_groupSeparator) { arrow == -1 ? position-- : position++; return position; } } // position cursor ahead of zero width space if (position > 1 && position < m_expression.size()) { if (m_expression.at(position - 1) == ZERO_WIDTH_SPACE.toString()) { arrow == -1 ? position-- : position++; return position; } } // position cursor around functions not between QRegularExpression re(QStringLiteral(R"([^\d+−\-×÷!%π∫√∛ˆ,\^()ˆ⁰¹²³⁴⁵⁶⁷⁸⁹ ]{2,})")); QRegularExpressionMatch match = re.match(m_expression.mid(position - 1, 2)); if (match.hasMatch()) { if (position == m_expression.size()) { // at end, do nothing } else if (m_expression.at(position - 1) == m_expression.at(position)) { // same char, do nothing } else { // check nearest left int posLeft = position - 1; while (posLeft >= 0) { if (m_expression.at(posLeft).isDigit() || m_expression.at(posLeft).isSymbol() || m_expression.at(posLeft) == QLatin1Char('(')) { posLeft++; break; } else if (posLeft == 0) { break; } else { posLeft--; } } // check nearest right int posRight = position + 1; while (posRight < m_expression.size()) { if (m_expression.at(posRight).isDigit() || m_expression.at(posRight).isSymbol() || m_expression.at(posRight) == QLatin1Char('(')) { break; } posRight++; } // prefer the closest side if (arrow != 1 && (position - posLeft < posRight - position || arrow == -1)) { position -= position - posLeft; } else { position += posRight - position; } return position; } } return position; } void InputManager::append(const QString &subexpression) { // if expression was from result and input is numeric, clear expression if (m_moveFromResult && subexpression.size() == 1 && m_inputPosition == m_input.size()) { if (subexpression.at(0).isDigit() || subexpression.at(0) == QLatin1Char('.')) { m_input.clear(); } } m_moveFromResult = false; QString temp = subexpression; temp.remove(m_groupSeparator); // auto parentheses if (temp == QStringLiteral("( )")) { temp = QStringLiteral("("); if (m_input.size() > 0 && m_input.at(m_inputPosition - 1) != QLatin1Char('(')) { QStringView left = m_input.left(m_inputPosition); if (left.count(QStringLiteral("(")) > left.count(QStringLiteral(")"))) { temp = QStringLiteral(")"); } } } else if (temp == QStringLiteral("rand()")) { bool isAprox = false; temp = m_engine->evaluate(temp, &isAprox); } // prevent invalid duplicate operators if (QStringLiteral("+×*÷/").contains(temp) && m_inputPosition > 0 && m_input.size() > 0 && m_input.at(m_inputPosition - 1) == temp) { return; } m_input.insert(m_inputPosition, temp); m_inputPosition += temp.size(); calculate(); store(); } void InputManager::backspace() { if (m_inputPosition < 1) { return; } // delete entire function if (m_input.size() > 2) { int posBack = m_inputPosition - 2; while (posBack >= 0) { if (m_input.at(posBack).isDigit() || m_input.at(posBack).isSymbol() || m_input.at(posBack).isPunct() || m_input.at(posBack).isSpace() || m_input.at(posBack + 1) == m_input.at(posBack)) { break; } posBack--; } const int diff = m_inputPosition - posBack; m_input.remove(m_inputPosition - diff + 1, diff - 1); m_inputPosition = m_inputPosition - diff + 1; } else { m_input.remove(m_inputPosition - 1, 1); m_inputPosition--; } calculate(); store(); } void InputManager::equal() { if (m_output.isEmpty()) { // if the output is empty, either the input is empty or user has pressed equal twice return; } Q_EMIT addHistory(m_expression + QStringLiteral(" = ") + m_result); QString savedResult = m_isBinaryMode ? m_binaryResult : m_result; if (m_isApproximate) { // Show fraction representation of result calculate(true); m_input = QStringLiteral("(") + m_output + QStringLiteral(")"); if (savedResult == m_result) { m_result.clear(); Q_EMIT resultChanged(); } } else { m_input = m_output; m_result.clear(); Q_EMIT resultChanged(); } m_output.clear(); m_expression = savedResult; m_binaryResult.clear(); m_hexResult.clear(); m_moveFromResult = true; m_inputPosition = m_input.size(); Q_EMIT expressionChanged(); Q_EMIT binaryResultChanged(); Q_EMIT hexResultChanged(); store(); } void InputManager::clear(bool save) { m_input.clear(); m_output.clear(); m_expression.clear(); m_result.clear(); m_binaryResult.clear(); m_hexResult.clear(); m_inputPosition = 0; Q_EMIT expressionChanged(); Q_EMIT resultChanged(); Q_EMIT binaryResultChanged(); Q_EMIT hexResultChanged(); if (save) { store(); } } void InputManager::setHistoryIndex(const int &index) { m_historyIndex = index; } int InputManager::historyIndex() const { return m_historyIndex; } void InputManager::fromHistory(const QString &result) { m_input = result; m_input.remove(m_groupSeparator); m_output.clear(); m_expression = result; m_result.clear(); m_binaryResult.clear(); m_hexResult.clear(); m_moveFromResult = true; m_inputPosition = m_input.size(); Q_EMIT expressionChanged(); Q_EMIT resultChanged(); Q_EMIT binaryResultChanged(); Q_EMIT hexResultChanged(); } void InputManager::store() { if (m_undoStack.size() > m_undoPos) { m_undoStack.resize(m_undoPos); m_undoStack.shrink_to_fit(); } m_undoStack.push_back(m_input); m_undoPos++; Q_EMIT canUndoChanged(); Q_EMIT canRedoChanged(); } void InputManager::undo() { if (m_undoPos <= 0) { return; } m_undoPos--; if (m_undoPos >= 1) { m_input = m_undoStack.at(m_undoPos - 1); m_inputPosition += m_input.size() - m_undoStack.at(m_undoPos).size(); } else { m_input = QString(); } if (m_inputPosition > m_input.size()) { m_inputPosition = m_input.size(); } calculate(); Q_EMIT canUndoChanged(); Q_EMIT canRedoChanged(); } void InputManager::redo() { if (m_undoPos >= m_undoStack.size()) { return; } m_inputPosition += m_undoStack.at(m_undoPos).size() - m_input.size(); m_input = m_undoStack.at(m_undoPos); m_undoPos++; if (m_inputPosition > m_input.size()) { m_inputPosition = m_input.size(); } calculate(); Q_EMIT canUndoChanged(); Q_EMIT canRedoChanged(); } bool InputManager::canUndo() { return m_undoPos > 0; } bool InputManager::canRedo() { return m_undoPos < m_undoStack.size(); } void InputManager::setBinaryMode(bool active) { m_isBinaryMode = active; clear(); } bool InputManager::binaryMode() { return m_isBinaryMode; } QString InputManager::formatNumbers(const QString &text) { QString temp = text; // show exponents as superscripts if (temp.contains(QStringLiteral("^"))) { QRegularExpression re(QStringLiteral(R"((?\w+|\)|!|π|%)?(?\^-?[\d.]+(?!\!)))")); QRegularExpressionMatchIterator i = re.globalMatch(temp); while (i.hasNext()) { QRegularExpressionMatch match = i.next(); QString base = match.captured(QStringLiteral("base")); QString exponent = match.captured(QStringLiteral("exponent")); if (!base.isEmpty()) { exponent.replace(QStringLiteral("^"), ZERO_WIDTH_SPACE.toString()); } replaceWithSuperscript(exponent); // replace only the first occurence size_t index = temp.indexOf(match.captured(0)); temp.replace(index, match.captured(0).size(), base + exponent); } } QString formatted; QString number; for (const auto ch : temp) { if (ch.isDigit() || ch == m_decimalPoint || ch == FRACTION_SLASH.toString()) { number.append(ch); } else { // do not add number separators if contains fraction if (!number.isEmpty()) { if (!number.contains(FRACTION_SLASH.toString())) { addNumberSeparators(number); } formatted.append(number); number.clear(); } formatted.append(ch); } } // do not add number separators if contains fraction if (!number.isEmpty()) { if (!number.contains(FRACTION_SLASH.toString())) { addNumberSeparators(number); } formatted.append(number); } formatted.replace(QStringLiteral("log10"), QStringLiteral("log₁₀")); formatted.replace(QStringLiteral("log2"), QStringLiteral("log₂")); return formatted; } void InputManager::replaceWithSuperscript(QString &text) { text.replace(QStringLiteral("^"), QStringLiteral("ˆ")); text.replace(QStringLiteral("0"), QStringLiteral("⁰")); text.replace(QStringLiteral("1"), QStringLiteral("¹")); text.replace(QStringLiteral("2"), QStringLiteral("²")); text.replace(QStringLiteral("3"), QStringLiteral("³")); text.replace(QStringLiteral("4"), QStringLiteral("⁴")); text.replace(QStringLiteral("5"), QStringLiteral("⁵")); text.replace(QStringLiteral("6"), QStringLiteral("⁶")); text.replace(QStringLiteral("7"), QStringLiteral("⁷")); text.replace(QStringLiteral("8"), QStringLiteral("⁸")); text.replace(QStringLiteral("9"), QStringLiteral("⁹")); text.replace(QStringLiteral("−"), QStringLiteral("⁻")); text.replace(QStringLiteral("-"), QStringLiteral("⁻")); } void InputManager::addNumberSeparators(QString &number) { const int idx = number.indexOf(m_decimalPoint); if (idx >= 0 && idx < number.size() - 3) { QString left = number.left(idx); QString right = number.right(number.size() - idx); left.replace(QRegularExpression(QStringLiteral(R"(\B(?=(\d{3})+(?!\d)))")), m_groupSeparator); number = left + right; } else if (number.size() > 3) { number.replace(QRegularExpression(QStringLiteral(R"(\B(?=(\d{3})+(?!\d)))")), m_groupSeparator); } } void InputManager::calculate(bool exact, const int minExp) { if (m_input.length() == 0) { clear(false); return; } m_expression = m_isBinaryMode ? m_input : formatNumbers(m_input); Q_EMIT expressionChanged(); QString input = m_input.trimmed(); m_isApproximate = false; if (m_isBinaryMode) { m_output = m_engine->evaluate(input, &m_isApproximate, 2, 10, exact, minExp); m_binaryResult = m_engine->evaluate(input, &m_isApproximate, 2, 2, exact, minExp); m_hexResult = m_engine->evaluate(input, &m_isApproximate, 2, 16, exact, minExp); Q_EMIT binaryResultChanged(); Q_EMIT hexResultChanged(); } else { m_output = m_engine->evaluate(input, &m_isApproximate, 10, 10, exact, minExp); } m_result = formatNumbers(m_output); Q_EMIT resultChanged(); } #include "moc_inputmanager.cpp"