qtdeclarative/examples/quickcontrols/spreadsheets/Spreadsheets/spreadmodel.cpp

732 lines
25 KiB
C++
Raw Normal View History

// 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 <QPoint>
#include <QRegularExpression>
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<int, QByteArray> 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<char>(view_section + A)};
} else {
const int first = view_section / 26 - 1;
const int second = view_section % 26;
QString title{static_cast<char>(first + A)};
title += static_cast<char>(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<int, int> 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;
}
}
}