// Copyright (C) 2024 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause #include "spreadformula.h" #include "spreadmodel.h" #include "spreadrole.h" #include #include int SpreadModel::columnNumberFromName(const QString &text) { if (text.size() == 1) return getModelColumn(text[0].toLatin1() - 'A'); else if (text.size() == 2) return getModelColumn((text[0].toLatin1() - 'A' + 1) * 26 + (text[1].toLatin1() - 'A')); return 0; } int SpreadModel::rowNumberFromName(const QString &text) { return getModelRow(text.toInt() - 1); } SpreadModel *SpreadModel::instance() { return s_instance; } SpreadModel *SpreadModel::create(QQmlEngine *, QJSEngine *) { if (!s_instance) s_instance = new SpreadModel {nullptr}; return s_instance; } void SpreadModel::update(int row, int column) { if (!m_updateMutex.tryLock()) return; column = getModelColumn(column); row = getModelRow(row); const QModelIndex index = this->index(row, column); emit dataChanged(index, index, {spread::Role::Display,}); m_updateMutex.unlock(); } void SpreadModel::clearHighlight() { if (m_dataModel.empty()) return; const auto [top_left, bottom_right] = m_dataModel.clearHighlight(); emit dataChanged(index(top_left.first, top_left.second), index(bottom_right.first, bottom_right.second), {spread::Role::Hightlight,}); } void SpreadModel::setHighlight(const QModelIndex &index, bool highlight) { if (!index.isValid()) return; if (m_dataModel.setHighlight(SpreadKey{index.row(), index.column()}, highlight)) emit dataChanged(index, index, {spread::Role::Hightlight,}); } int SpreadModel::rowCount(const QModelIndex &parent) const { return m_size.first; } int SpreadModel::columnCount(const QModelIndex &parent) const { return m_size.second; } QVariant SpreadModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant{}; const int row = index.row(); const int column = index.column(); return m_dataModel.getData(SpreadKey{row, column}, role); } bool SpreadModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (!index.isValid()) return false; const int row = index.row(); const int column = index.column(); if (m_dataModel.setData(SpreadKey{row, column}, value, role)) emit dataChanged(index, index); return true; } bool SpreadModel::clearItemData(const QModelIndex &index) { if (!index.isValid()) return false; if (m_dataModel.clearData(SpreadKey{index.row(), index.column()})) { emit dataChanged(index, index); return true; } return false; } Qt::ItemFlags SpreadModel::flags(const QModelIndex &index) const { if (!index.isValid()) return Qt::NoItemFlags; return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable; } QHash SpreadModel::roleNames() const { return {{spread::Role::Display, "display"}, {spread::Role::Edit, "edit"}, {spread::Role::ColumnName, "columnName"}, {spread::Role::RowName, "rowName"}, {spread::Role::Hightlight, "highlight"}}; } QVariant SpreadModel::headerData(int section, Qt::Orientation orientation, int role) const { switch (role) { case spread::Role::ColumnName: case spread::Role::RowName: break; default: return QVariant{}; } switch (orientation) { case Qt::Horizontal: { constexpr char A = 'A'; const int view_section = getViewColumn(section); if (view_section < 26) { return QString{static_cast(view_section + A)}; } else { const int first = view_section / 26 - 1; const int second = view_section % 26; QString title{static_cast(first + A)}; title += static_cast(second + A); return title; } } case Qt::Vertical: { return getViewRow(section) + 1; } } return QVariant{}; } bool SpreadModel::insertColumns(int column, int count, const QModelIndex &parent) { if (count != 1) // TODO: implement inserting more than 1 columns return false; beginInsertColumns(QModelIndex{}, column, column + count - 1); m_size.second += count; // update model m_dataModel.shiftColumns(column, count); QMap old_columns; std::swap(old_columns, m_viewColumns); for (auto it = old_columns.begin(); it != old_columns.end(); ++it) { const int new_view_column = (it.value() < column) ? it.value() : it.value() + count; const int new_model_column = (it.key() < column) ? it.key() : it.key() + count; m_viewColumns.insert(new_model_column, new_view_column); } endInsertColumns(); return true; } bool SpreadModel::insertRows(int row, int count, const QModelIndex &parent) { if (count != 1) // TODO: implement inserting more than 1 rows return false; beginInsertRows(QModelIndex{}, row, row + count - 1); m_size.first += count; // update model m_dataModel.shiftRows(row, count); endInsertRows(); return true; } bool SpreadModel::removeColumns(int column, int count, const QModelIndex &parent) { if (count != 1) // TODO: implement removing more than 1 columns return false; beginRemoveColumns(QModelIndex{}, column, column + count - 1); m_size.second -= count; // update model m_dataModel.removeColumnCells(column); m_dataModel.shiftColumns(column + 1, -count); endRemoveColumns(); return true; } bool SpreadModel::removeRows(int row, int count, const QModelIndex &parent) { if (count != 1) // TODO: implement removing more than 1 rows return false; beginRemoveRows(QModelIndex{}, row, row + count - 1); m_size.first -= count; // update model m_dataModel.removeRowCells(row); m_dataModel.shiftRows(row + 1, -count); endRemoveRows(); return true; } bool SpreadModel::clearItemData(const QModelIndexList &indexes) { bool ok = true; for (const QModelIndex &index : indexes) ok &= clearItemData(index); return ok; } bool SpreadModel::removeColumns(QModelIndexList indexes) { auto greater = [](const QModelIndex &lhs, const QModelIndex &rhs) { return lhs.column() > rhs.column(); }; std::sort(indexes.begin(), indexes.end(), greater); for (const QModelIndex &index : indexes) removeColumn(index.column()); return true; } bool SpreadModel::removeRows(QModelIndexList indexes) { auto greater = [](const QModelIndex &lhs, const QModelIndex &rhs) { return lhs.row() > rhs.row(); }; std::sort(indexes.begin(), indexes.end(), greater); for (const QModelIndex &index : indexes) removeRow(index.row()); return true; } void SpreadModel::mapColumn(int model, int view) { if (model == view) m_viewColumns.remove(model); else m_viewColumns[model] = view; emit headerDataChanged(Qt::Horizontal, model, view); } void SpreadModel::mapRow(int model, int view) { if (model == view) m_viewRows.remove(model); else m_viewRows[model] = view; emit headerDataChanged(Qt::Vertical, model, view); } Formula SpreadModel::parseFormulaString(const QString &qText) { if (qText.isEmpty()) return Formula{}; QRegularExpression pattern_re; // is formula pattern_re.setPattern("^\\s*=.*"); QRegularExpressionMatch match = pattern_re.match(qText); if (!match.hasMatch()) return Formula{}; // is Assignment: e.g. =A1 pattern_re.setPattern("^\\s*=\\s*([a-zA-Z]+)([1-9][0-9]*)\\s*$"); match = pattern_re.match(qText); if (match.hasMatch()) { const QString column_label = match.captured(1); const QString row_label = match.captured(2); const int column = columnNumberFromName(column_label); const int row = rowNumberFromName(row_label); const int cell_id = m_dataModel.createId(SpreadKey{row, column}); return Formula::create(Formula::Operator::Assign, cell_id, 0); } // is Addition: e.g. =A1+A2 pattern_re.setPattern("^\\s*=\\s*([a-zA-Z]+)([1-9][0-9]*)\\s*\\+\\s*([a-zA-Z]+)([1-9][0-9]*)\\s*$"); match = pattern_re.match(qText); if (match.hasMatch()) { // first argument QString column_label = match.captured(1); QString row_label = match.captured(2); int column = columnNumberFromName(column_label); int row = rowNumberFromName(row_label); const int cell_id_1 = m_dataModel.createId(SpreadKey{row, column}); // second argument column_label = match.captured(3); row_label = match.captured(4); column = columnNumberFromName(column_label); row = rowNumberFromName(row_label); const int cell_id_2 = m_dataModel.createId(SpreadKey{row, column}); // create formula return Formula::create(Formula::Operator::Add, cell_id_1, cell_id_2); } // is Subtraction: e.g. =A1-A2 pattern_re.setPattern("^\\s*=\\s*([a-zA-Z]+)([1-9][0-9]*)\\s*\\-\\s*([a-zA-Z]+)([1-9][0-9]*)\\s*$"); match = pattern_re.match(qText); if (match.hasMatch()) { // first argument QString column_label = match.captured(1); QString row_label = match.captured(2); int column = columnNumberFromName(column_label); int row = rowNumberFromName(row_label); const int cell_id_1 = m_dataModel.createId(SpreadKey{row, column}); // second argument column_label = match.captured(3); row_label = match.captured(4); column = columnNumberFromName(column_label); row = rowNumberFromName(row_label); const int cell_id_2 = m_dataModel.createId(SpreadKey{row, column}); // create formula return Formula::create(Formula::Operator::Sub, cell_id_1, cell_id_2); } // is Multiply: e.g. =A1*A2 pattern_re.setPattern("^\\s*=\\s*([a-zA-Z]+)([1-9][0-9]*)\\s*\\*\\s*([a-zA-Z]+)([1-9][0-9]*)\\s*$"); match = pattern_re.match(qText); if (match.hasMatch()) { // first argument QString column_label = match.captured(1); QString row_label = match.captured(2); int column = columnNumberFromName(column_label); int row = rowNumberFromName(row_label); const int cell_id_1 = m_dataModel.createId(SpreadKey{row, column}); // second argument column_label = match.captured(3); row_label = match.captured(4); column = columnNumberFromName(column_label); row = rowNumberFromName(row_label); const int cell_id_2 = m_dataModel.createId(SpreadKey{row, column}); // create formula return Formula::create(Formula::Operator::Mul, cell_id_1, cell_id_2); } // is Division: e.g. =A1/A2 pattern_re.setPattern("^\\s*=\\s*([a-zA-Z]+)([1-9][0-9]*)\\s*\\/\\s*([a-zA-Z]+)([1-9][0-9]*)\\s*$"); match = pattern_re.match(qText); if (match.hasMatch()) { // first argument QString column_label = match.captured(1); QString row_label = match.captured(2); int column = columnNumberFromName(column_label); int row = rowNumberFromName(row_label); const int cell_id_1 = m_dataModel.createId(SpreadKey{row, column}); // second argument column_label = match.captured(3); row_label = match.captured(4); column = columnNumberFromName(column_label); row = rowNumberFromName(row_label); const int cell_id_2 = m_dataModel.createId(SpreadKey{row, column}); // create formula return Formula::create(Formula::Operator::Div, cell_id_1, cell_id_2); } // is Summation: e.g. =SUM A1:A2 pattern_re.setPattern("^\\s*=\\s*[Ss][Uu][Mm]\\s+([a-zA-Z]+)([1-9][0-9]*)\\s*\\:\\s*([a-zA-Z]+)([1-9][0-9]*)\\s*$"); match = pattern_re.match(qText); if (match.hasMatch()) { // first argument QString column_label = match.captured(1); QString row_label = match.captured(2); int column = columnNumberFromName(column_label); int row = rowNumberFromName(row_label); const int cell_id_1 = m_dataModel.createId(SpreadKey{row, column}); // second argument column_label = match.captured(3); row_label = match.captured(4); column = columnNumberFromName(column_label); row = rowNumberFromName(row_label); const int cell_id_2 = m_dataModel.createId(SpreadKey{row, column}); // create formula return Formula::create(Formula::Operator::Sum, cell_id_1, cell_id_2); } return Formula{}; } QString SpreadModel::formulaValueText(const Formula &formula) { switch (formula.getOperator()) { case Formula::Operator::Assign: { const QVariant value = m_dataModel.getData(formula.firstOperandId(), spread::Role::Display); return value.isNull() ? QString{} : value.toString(); } case Formula::Operator::Add: { const QVariant value_1 = m_dataModel.getData(formula.firstOperandId(), spread::Role::Display); const QVariant value_2 = m_dataModel.getData(formula.secondOperandId(), spread::Role::Display); if (value_1.isNull() && value_2.isNull()) return "0"; // check int values bool is_int_1 = true; bool is_int_2 = true; const int int_1 = value_1.isNull() ? 0 : value_1.toInt(&is_int_1); const int int_2 = value_2.isNull() ? 0 : value_2.toInt(&is_int_2); if (is_int_1 && is_int_2) return QString::number(int_1 + int_2); // check double values bool is_double_1 = true; bool is_double_2 = true; const double double_1 = value_1.isNull() ? 0 : value_1.toDouble(&is_double_1); const double double_2 = value_2.isNull() ? 0 : value_2.toDouble(&is_double_2); if (is_double_1 && is_double_2) return QString::number(double_1 + double_2); return "#ERROR!"; } case Formula::Operator::Sub: { const QVariant value_1 = m_dataModel.getData(formula.firstOperandId(), spread::Role::Display); const QVariant value_2 = m_dataModel.getData(formula.secondOperandId(), spread::Role::Display); if (value_1.isNull() && value_2.isNull()) return "0"; // check int values bool is_int_1 = true; bool is_int_2 = true; const int int_1 = value_1.isNull() ? 0 : value_1.toInt(&is_int_1); const int int_2 = value_2.isNull() ? 0 : value_2.toInt(&is_int_2); if (is_int_1 && is_int_2) return QString::number(int_1 - int_2); // check double values bool is_double_1 = true; bool is_double_2 = true; const double double_1 = value_1.isNull() ? 0 : value_1.toDouble(&is_double_1); const double double_2 = value_2.isNull() ? 0 : value_2.toDouble(&is_double_2); if (is_double_1 && is_double_2) return QString::number(double_1 - double_2); return "#ERROR!"; } case Formula::Operator::Mul: { const QVariant value_1 = m_dataModel.getData(formula.firstOperandId(), spread::Role::Display); const QVariant value_2 = m_dataModel.getData(formula.secondOperandId(), spread::Role::Display); if (value_1.isNull() || value_2.isNull()) return "0"; // check int values bool is_int_1 = true; bool is_int_2 = true; const int int_1 = value_1.toInt(&is_int_1); const int int_2 = value_2.toInt(&is_int_2); if (is_int_1 && is_int_2) return QString::number(int_1 * int_2); // check double values bool is_double_1 = true; bool is_double_2 = true; const double double_1 = value_1.toDouble(&is_double_1); const double double_2 = value_2.toDouble(&is_double_2); if (is_double_1 && is_double_2) return QString::number(double_1 * double_2); return "#ERROR!"; } case Formula::Operator::Div: { const QVariant value_1 = m_dataModel.getData(formula.firstOperandId(), spread::Role::Display); const QVariant value_2 = m_dataModel.getData(formula.secondOperandId(), spread::Role::Display); if (value_1.isNull() && value_2.isNull()) return "#ERROR!"; // check int values bool is_int_1 = true; bool is_int_2 = true; const int int_1 = value_1.isNull() ? 0 : value_1.toInt(&is_int_1); const int int_2 = value_2.isNull() ? 0 : value_2.toInt(&is_int_2); if (is_int_1 && is_int_2) { if (int_2 == 0) return "#ERROR!"; return QString::number(int_1 / int_2); } // check double values bool is_double_1 = true; bool is_double_2 = true; const double double_1 = value_1.isNull() ? 0 : value_1.toDouble(&is_double_1); const double double_2 = value_2.isNull() ? 0 : value_2.toDouble(&is_double_2); if (is_double_1 && is_double_2) { if (double_2 == 0) return "#ERROR!"; return QString::number(double_1 / double_2); } return "#ERROR!"; } case Formula::Operator::Sum: { SpreadKey top_left = m_dataModel.getKey(formula.firstOperandId()); SpreadKey bottom_right = m_dataModel.getKey(formula.secondOperandId()); top_left.first = getViewRow(top_left.first); top_left.second = getViewColumn(top_left.second); bottom_right.first = getViewRow(bottom_right.first); bottom_right.second = getViewColumn(bottom_right.second); if (bottom_right.first < top_left.first) std::swap(top_left.first, bottom_right.first); if (bottom_right.second < top_left.second) std::swap(top_left.second, bottom_right.second); double sum = 0; for (int row = top_left.first; row <= bottom_right.first; ++row) { for (int column = top_left.second; column <= bottom_right.second; ++column) { const int model_row = getModelRow(row); const int model_column = getModelColumn(column); const SpreadKey key {model_row, model_column}; const QVariant value = m_dataModel.getData(key, spread::Role::Display); if (value.isNull()) continue; bool is_double = false; const double d = value.toDouble(&is_double); if (is_double) sum += d; else return "#ERROR!"; } } return QString::number(sum); } default: break; } return QString{}; } int SpreadModel::getViewColumn(int modelColumn) const { auto it = m_viewColumns.find(modelColumn); return (it != m_viewColumns.end()) ? it.value() : modelColumn; } int SpreadModel::getModelColumn(int viewColumn) const { auto find_view_column = [viewColumn](const auto &item) { return item == viewColumn; }; auto it = std::find_if(m_viewColumns.begin(), m_viewColumns.end(), find_view_column); return it != m_viewColumns.end() ? it.key() : viewColumn; } int SpreadModel::getViewRow(int modelRow) const { auto it = m_viewRows.find(modelRow); return (it != m_viewRows.end()) ? it.value() : modelRow; } int SpreadModel::getModelRow(int viewRow) const { auto find_view_row = [viewRow](const auto &item){ return item == viewRow; }; auto it = std::find_if(m_viewRows.begin(), m_viewRows.end(), find_view_row); return (it != m_viewRows.end()) ? it.key() : viewRow; } void SpreadSelectionModel::toggleColumn(int column) { isColumnSelected(column) ? deselectColumn(column) : selectColumn(column, false); } void SpreadSelectionModel::deselectColumn(int column) { const QAbstractItemModel *model = this->model(); const QModelIndex first = model->index(0, column); const QModelIndex last = model->index(model->rowCount() - 1, column); QModelIndexList selectedRows = this->selectedRows(column); if (selectedRows.empty()) { select(QItemSelection{first, last}, SelectionFlag::Deselect); return; } auto topToBottom = [](const QModelIndex &lhs, const QModelIndex &rhs) -> bool { return lhs.row() < rhs.row(); }; std::sort(selectedRows.begin(), selectedRows.end(), topToBottom); QModelIndex index = first; for (const QModelIndex &selectedRow : selectedRows) { if (index.row() < selectedRow.row()) select(QItemSelection{index, model->index(selectedRow.row() - 1, column)}, SelectionFlag::Deselect); index = model->index(selectedRow.row() + 1, column); } if (index.row() <= last.row()) select(QItemSelection{index, last}, SelectionFlag::Deselect); } void SpreadSelectionModel::selectColumn(int column, bool clear) { if (clear) this->clear(); const QAbstractItemModel *model = this->model(); const QModelIndex first = model->index(0, column); const QModelIndex last = model->index(model->rowCount() - 1, column); select(QItemSelection{first, last}, SelectionFlag::Select); } void SpreadSelectionModel::toggleRow(int row) { isRowSelected(row) ? deselectRow(row) : selectRow(row, false); } void SpreadSelectionModel::deselectRow(int row) { const QAbstractItemModel *model = this->model(); const QModelIndex first = model->index(row, 0); const QModelIndex last = model->index(row, model->columnCount() - 1); QModelIndexList selectedColumns = this->selectedColumns(row); if (selectedColumns.empty()) { select(QItemSelection{first, last}, SelectionFlag::Deselect); return; } auto leftToRight = [](const QModelIndex &lhs, const QModelIndex &rhs) -> bool { return lhs.column() < rhs.column(); }; std::sort(selectedColumns.begin(), selectedColumns.end(), leftToRight); QModelIndex index = first; for (const QModelIndex &selectedColumn : selectedColumns) { if (index.column() < selectedColumn.column()) select(QItemSelection{index, model->index(row, selectedColumn.column() - 1)}, SelectionFlag::Deselect); index = model->index(row, selectedColumn.column() + 1); } if (index.column() <= last.column()) select(QItemSelection{index, last}, SelectionFlag::Deselect); } void SpreadSelectionModel::selectRow(int row, bool clear) { if (clear) this->clear(); const QAbstractItemModel *model = this->model(); const QModelIndex first = model->index(row, 0); const QModelIndex last = model->index(row, model->columnCount() - 1); select(QItemSelection{first, last}, SelectionFlag::Select); } void SpreadSelectionModel::setBehavior(Behavior behavior) { if (behavior == m_behavior) return; m_behavior = behavior; emit behaviorChanged(); } void HeaderSelectionModel::setCurrent(int current) { switch (m_orientation) { case Qt::Horizontal: QItemSelectionModel::setCurrentIndex(model()->index(0, current), SelectionFlag::Current); break; case Qt::Vertical: QItemSelectionModel::setCurrentIndex(model()->index(current, 0), SelectionFlag::Current); break; default: break; } } void HeaderSelectionModel::setSelectionModel(SpreadSelectionModel *selectionModel) { if (selectionModel == m_selectionModel) return; if (m_selectionModel) disconnect(m_selectionModel); m_selectionModel = selectionModel; if (m_selectionModel) connect(m_selectionModel, &SpreadSelectionModel::selectionChanged, this, &HeaderSelectionModel::onSelectionChanged); emit selectionModelChanged(); } void HeaderSelectionModel::setOrientation(Qt::Orientation orientation) { if (orientation == m_orientation) return; m_orientation = orientation; emit orientationChanged(); } void HeaderSelectionModel::onSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected) { const QAbstractItemModel *model = this->model(); for (const QModelIndex &index : selected.indexes()) { switch (m_orientation) { case Qt::Horizontal: if (m_selectionModel->isColumnSelected(index.column())) select(model->index(0, index.column()), SelectionFlag::Select); break; case Qt::Vertical: if (m_selectionModel->isRowSelected(index.row())) select(model->index(index.row(), 0), SelectionFlag::Select); break; } } for (const QModelIndex &index : deselected.indexes()) { switch (m_orientation) { case Qt::Horizontal: if (!m_selectionModel->isColumnSelected(index.column())) select(model->index(0, index.column()), SelectionFlag::Deselect); break; case Qt::Vertical: if (!m_selectionModel->isRowSelected(index.row())) select(model->index(index.row(), 0), SelectionFlag::Deselect); break; } } }