diff --git a/CMakeLists.txt b/CMakeLists.txt index 182468c..d5492fe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,7 +10,7 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(QT NAMES Qt6 REQUIRED COMPONENTS Core LinguistTools) -find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core LinguistTools) +find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core LinguistTools Gui) configure_file(CoreConfig.h.in CoreConfig.h) @@ -25,11 +25,14 @@ add_library(${TARGET_APP} STATIC model/tablemodel.h model/tablemodel.cpp model/modelitem.h model/modelitem.cpp formats/jsonparser.h formats/jsonparser.cpp + model/commands/insertrowscommand.h model/commands/insertrowscommand.cpp + model/commands/removerowscommand.h model/commands/removerowscommand.cpp + model/commands/edititemcommand.h model/commands/edititemcommand.cpp ) include_directories(${CMAKE_CURRENT_BINARY_DIR}) -target_link_libraries(${TARGET_APP} PRIVATE Qt${QT_VERSION_MAJOR}::Core) +target_link_libraries(${TARGET_APP} PRIVATE Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Gui) target_compile_definitions(${TARGET_APP} PRIVATE ${TARGET_APP}_LIBRARY) diff --git a/genericcore.cpp b/genericcore.cpp index 3cd0747..4cf0500 100644 --- a/genericcore.cpp +++ b/genericcore.cpp @@ -12,11 +12,16 @@ #include "constants.h" #include "model/tablemodel.h" +#include + using namespace std; GenericCore::GenericCore() { qDebug() << "Creating core..."; + // TODO let the model own its undo stack (& use TableModel::getUndoStack() if necessary) + m_modelUndoStack = new QUndoStack(this); + setupModels(); } @@ -70,10 +75,12 @@ void GenericCore::triggerApplicationUpdate() { QProcess::startDetached(toolFilePath, args); } +QUndoStack* GenericCore::getModelUndoStack() const { return m_modelUndoStack; } + std::shared_ptr GenericCore::getModel() const { return m_mainModel; } void GenericCore::setupModels() { - m_mainModel = make_shared(this); + m_mainModel = make_shared(m_modelUndoStack, this); // TODO add QAbstractItemModelTester } diff --git a/genericcore.h b/genericcore.h index 27fc94c..20ac805 100644 --- a/genericcore.h +++ b/genericcore.h @@ -3,6 +3,7 @@ #include +class QUndoStack; class QAbstractItemModel; class QString; @@ -21,12 +22,14 @@ class GenericCore : public QObject { bool isApplicationUpdateAvailable(); void triggerApplicationUpdate(); + QUndoStack* getModelUndoStack() const; std::shared_ptr getModel() const; signals: void displayStatusMessage(QString message); private: + QUndoStack* m_modelUndoStack; std::shared_ptr m_mainModel; void setupModels(); diff --git a/model/commands/edititemcommand.cpp b/model/commands/edititemcommand.cpp new file mode 100644 index 0000000..4c57bc1 --- /dev/null +++ b/model/commands/edititemcommand.cpp @@ -0,0 +1,79 @@ +#include "edititemcommand.h" + +#include + +#include "../tablemodel.h" + +EditItemCommand::EditItemCommand(TableModel* model, + const QModelIndex& index, + QMap& changedValues, + QUndoCommand* parent) + : QUndoCommand(parent) + , m_model(model) + , m_row(index.row()) { + qInfo() << "New EditCommand..."; + QString commandText; + + if (changedValues.size() == 1) { + qDebug() << "Only one value to change. Using more specific command text..."; + const int role = changedValues.firstKey(); + const QVariant value = changedValues.first(); + QString roleName = model->roleNames().value(role); + switch (role) { + case TableModel::NameRole: + case TableModel::DescriptionRole: + case TableModel::InfoRole: + case TableModel::AmountRole: + case TableModel::FactorRole: + commandText = QString("Setting '%1' of item '%2' to '%3'") + .arg(roleName) + .arg(index.data(TableModel::NameRole).toString()) + .arg(value.toString()); + break; + default: + commandText = QString("Edit item '%1'").arg(index.data(TableModel::NameRole).toString()); + break; + } + } else { + qDebug() << "More than one value to change. Using a generic command text..."; + commandText = QString("Edit item '%1'").arg(index.data(TableModel::NameRole).toString()); + } + setText(commandText); + + m_newValues = changedValues; + + /// storing old values for undo step + m_oldValues = getOldValues(index, changedValues); +} + +void EditItemCommand::undo() { + qDebug() << "Undoing the EditCommand..."; + m_model->execEditItemData(m_row, m_oldValues); +} + +void EditItemCommand::redo() { + qDebug() << "(Re-)doing the EditCommand..."; + m_model->execEditItemData(m_row, m_newValues); +} + +const QMap EditItemCommand::getOldValues( + const QModelIndex& index, + const QMap& changedValues) const { + QMap result; + QMap::const_iterator i; + for (i = changedValues.constBegin(); i != changedValues.constEnd(); ++i) { + const int role = i.key(); + const QVariant newValue = i.value(); + const QVariant oldValue = index.data(role); + // TODO check if role is a editable role? + if (oldValue != newValue) { + qDebug() << "oldValue:" << oldValue << "!= newValue:" << newValue; + result.insert(role, oldValue); + } else { + qInfo() << "oldValue is already the same as newValue:" << oldValue; + } + } + // QVariant oldModifiedDate = index.data(ModifiedDateUTCRole); + // result.insert(ModifiedDateUTCRole, oldModifiedDate); + return result; +} diff --git a/model/commands/edititemcommand.h b/model/commands/edititemcommand.h new file mode 100644 index 0000000..a8a2c23 --- /dev/null +++ b/model/commands/edititemcommand.h @@ -0,0 +1,30 @@ +#ifndef EDITITEMCOMMAND_H +#define EDITITEMCOMMAND_H + +#include +#include + +class TableModel; + +class EditItemCommand : public QUndoCommand { + public: + EditItemCommand(TableModel* model, + const QModelIndex& index, + QMap& changedValues, + QUndoCommand* parent = nullptr); + /// QUndoCommand interface + void undo(); + void redo(); + + private: + TableModel* m_model = nullptr; + const int m_row; + QMap m_oldValues; + QMap m_newValues; + + /// private functions + const QMap getOldValues(const QModelIndex& index, + const QMap& changedValues) const; +}; + +#endif // EDITITEMCOMMAND_H diff --git a/model/commands/insertrowscommand.cpp b/model/commands/insertrowscommand.cpp new file mode 100644 index 0000000..231ce29 --- /dev/null +++ b/model/commands/insertrowscommand.cpp @@ -0,0 +1,33 @@ +#include "insertrowscommand.h" + +#include + +#include "../tablemodel.h" + +InsertRowsCommand::InsertRowsCommand(TableModel* model, + int startRow, + QList > valueList, + QUndoCommand* parent) + : QUndoCommand(parent) + , m_tableModel(model) + , m_startRow(startRow) + , m_valueList(valueList) { + qInfo() << "New InsertCommand..."; + const QString commandText = + QString("inserting %1 item(s) on row %2").arg(valueList.length()).arg(startRow); + setText(commandText); +} + +void InsertRowsCommand::undo() { + qDebug() << "Undoing the InsertCommand..."; + if (m_tableModel) { + m_tableModel->execRemoveItems(m_startRow, m_valueList.length()); + } +} + +void InsertRowsCommand::redo() { + qDebug() << "(Re-)doing the InsertCommand..."; + if (m_tableModel) { + m_tableModel->execInsertItems(m_startRow, m_valueList); + } +} diff --git a/model/commands/insertrowscommand.h b/model/commands/insertrowscommand.h new file mode 100644 index 0000000..0341226 --- /dev/null +++ b/model/commands/insertrowscommand.h @@ -0,0 +1,28 @@ +#ifndef INSERTROWSCOMMAND_H +#define INSERTROWSCOMMAND_H + +#include + +class TableModel; + +class InsertRowsCommand : public QUndoCommand { + public: + // TODO don't use simple pointer to model + /// Using simple pointer to model because there was a crash when closing the application with an + /// unclean undo stack + InsertRowsCommand(TableModel* model, + int startRow, + QList > valueList, + QUndoCommand* parent = nullptr); + + /// QUndoCommand interface + void undo() override; + void redo() override; + + private: + TableModel* m_tableModel; + const int m_startRow; + const QList > m_valueList; +}; + +#endif // INSERTROWSCOMMAND_H diff --git a/model/commands/removerowscommand.cpp b/model/commands/removerowscommand.cpp new file mode 100644 index 0000000..306c9aa --- /dev/null +++ b/model/commands/removerowscommand.cpp @@ -0,0 +1,47 @@ +#include "removerowscommand.h" + +#include + +#include "../tablemodel.h" + +RemoveRowsCommand::RemoveRowsCommand(TableModel* model, + const int startRow, + const int nRows, + QUndoCommand* parent) + : QUndoCommand(parent) + , m_tableModel(model) + , m_startRow(startRow) { + qInfo() << "New RemoveCommand..."; + const QString commandText = + QString("removing %1 item(s) on position %2").arg(nRows).arg(startRow); + setText(commandText); + + for (int row = 0; row < nRows; ++row) { + const int rowPosition = startRow + row; + QModelIndex index = m_tableModel->index(rowPosition, 0); + + // TODO use a (static) function "getRoleValueHash" or something + QHash values; + values[TableModel::NameRole] = m_tableModel->data(index, TableModel::NameRole); + values[TableModel::DescriptionRole] = m_tableModel->data(index, TableModel::DescriptionRole); + values[TableModel::InfoRole] = m_tableModel->data(index, TableModel::InfoRole); + values[TableModel::AmountRole] = m_tableModel->data(index, TableModel::AmountRole); + values[TableModel::FactorRole] = m_tableModel->data(index, TableModel::FactorRole); + + m_valueList.append(values); + } +} + +void RemoveRowsCommand::undo() { + qDebug() << "Undoing the RemoveCommand..."; + if (m_tableModel) { + m_tableModel->execInsertItems(m_startRow, m_valueList); + } +} + +void RemoveRowsCommand::redo() { + qDebug() << "(Re-)doing the RemoveCommand..."; + if (m_tableModel) { + m_tableModel->execRemoveItems(m_startRow, m_valueList.length()); + } +} diff --git a/model/commands/removerowscommand.h b/model/commands/removerowscommand.h new file mode 100644 index 0000000..308f78b --- /dev/null +++ b/model/commands/removerowscommand.h @@ -0,0 +1,28 @@ +#ifndef REMOVEROWSCOMMAND_H +#define REMOVEROWSCOMMAND_H + +#include + +class TableModel; + +class RemoveRowsCommand : public QUndoCommand { + public: + // TODO don't use simple pointer to model + /// Using simple pointer to model because there was a crash when closing the application with an + /// unclean undo stack + RemoveRowsCommand(TableModel* model, + const int startRow, + const int nRows, + QUndoCommand* parent = nullptr); + + /// QUndoCommand interface + void undo() override; + void redo() override; + + private: + TableModel* m_tableModel; + const int m_startRow; + QList> m_valueList; +}; + +#endif // REMOVEROWSCOMMAND_H diff --git a/model/modelitem.cpp b/model/modelitem.cpp index b199dfc..a168b7f 100644 --- a/model/modelitem.cpp +++ b/model/modelitem.cpp @@ -16,3 +16,25 @@ bool ModelItem::setData(const QVariant& value, int role) { return valueChanged; } + +bool ModelItem::setItemData(const QMap& changedValues) { + bool valueChanged = false; + + QMap::const_iterator citer = changedValues.constBegin(); + + while (citer != changedValues.constEnd()) { + const int role = citer.key(); + const QVariant value = citer.value(); + + if (m_values.contains(role)) { + if (m_values.value(role) != value) { + valueChanged = true; + } + } + m_values[role] = value; + + citer++; + } + + return valueChanged; +} diff --git a/model/modelitem.h b/model/modelitem.h index 002eafd..6f9c366 100644 --- a/model/modelitem.h +++ b/model/modelitem.h @@ -8,8 +8,9 @@ class ModelItem { ModelItem(const QHash values); QVariant data(int role) const; - bool setData(const QVariant& value, int role); + // TODO change return value to list of changed roles + bool setItemData(const QMap& changedValues); private: QHash m_values; diff --git a/model/tablemodel.cpp b/model/tablemodel.cpp index 7a05900..675309a 100644 --- a/model/tablemodel.cpp +++ b/model/tablemodel.cpp @@ -1,6 +1,9 @@ #include "tablemodel.h" #include "../formats/jsonparser.h" +#include "commands/edititemcommand.h" +#include "commands/insertrowscommand.h" +#include "commands/removerowscommand.h" #include "modelitem.h" QHash TableModel::ROLE_NAMES = {{NameRole, "Name"}, @@ -8,9 +11,11 @@ QHash TableModel::ROLE_NAMES = {{NameRole, "Name"}, {InfoRole, "Info"}, {AmountRole, "Amount"}, {FactorRole, "Factor"}}; +QList TableModel::intColumns = {"Amount", "Factor"}; -TableModel::TableModel(QObject* parent) - : QAbstractTableModel{parent} { +TableModel::TableModel(QUndoStack* undoStack, QObject* parent) + : QAbstractTableModel{parent} + , m_undoStack(undoStack) { for (int row = 0; row < 5; ++row) { QHash values; values[NameRole] = QString("Item %1").arg(row); @@ -56,6 +61,12 @@ QVariant TableModel::data(const QModelIndex& index, int role) const { case Qt::DisplayRole: case Qt::EditRole: return m_items.at(row)->data(roleForColumn); + case NameRole: + case DescriptionRole: + case InfoRole: + case AmountRole: + case FactorRole: + return m_items.at(row)->data(role); } return QVariant(); @@ -75,18 +86,30 @@ QVariant TableModel::headerData(int section, Qt::Orientation orientation, int ro } bool TableModel::setData(const QModelIndex& index, const QVariant& value, int role) { - if (role == Qt::EditRole) { - if (!checkIndex(index)) { - return false; - } - int columnRole = getRoleForColumn(index.column()); - shared_ptr item = m_items.at(index.row()); - return item->setData(value, columnRole); + if (role == Qt::EditRole && checkIndex(index)) { + const int column = index.column(); + const int roleForColumn = getRoleForColumn(column); + return setItemData(index, {{roleForColumn, value}}); } return false; } -// bool TableModel::setItemData(const QModelIndex& index, const QMap& roles) {} +bool TableModel::setItemData(const QModelIndex& index, const QMap& roles) { + if (!checkIndex(index)) { + return false; + } + // if (isRoleReadOnly(roleForColumn)) { + // return false; + // } + + QMap changedValues = onlyChangedValues(index, roles); + if (changedValues.size() > 0) { + EditItemCommand* editCommand = new EditItemCommand(this, index, changedValues); + m_undoStack->push(editCommand); + return true; + } + return false; +} bool TableModel::removeRows(int firstRow, int nRows, const QModelIndex& parentIndex) { if (parentIndex != QModelIndex()) { @@ -100,9 +123,8 @@ bool TableModel::removeRows(int firstRow, int nRows, const QModelIndex& parentIn return false; } - beginRemoveRows(QModelIndex(), firstRow, lastRow); - m_items.remove(firstRow, nRows); - endRemoveRows(); + RemoveRowsCommand* removeCommand = new RemoveRowsCommand(this, firstRow, nRows); + m_undoStack->push(removeCommand); return true; } @@ -122,16 +144,48 @@ void TableModel::insertItems(int startPosition, } QList> valueList = JsonParser::toItemValuesList(jsonDoc); - const int nRows = valueList.size(); - beginInsertRows(QModelIndex(), startPosition, startPosition + nRows - 1); + + InsertRowsCommand* insertCommand = new InsertRowsCommand(this, startPosition, valueList); + m_undoStack->push(insertCommand); +} + +void TableModel::execInsertItems(const int firstRow, const QList> valueList) { + const int nRows = valueList.size(); + qDebug() << "Inserting" << nRows << "items..."; + + const int lastRow = firstRow + nRows - 1; + beginInsertRows(QModelIndex(), firstRow, lastRow); for (int row = 0; row < nRows; ++row) { - const int rowPosition = startPosition + row; + const int rowPosition = firstRow + row; shared_ptr item = make_unique(valueList.at(row)); m_items.insert(rowPosition, std::move(item)); } endInsertRows(); } +void TableModel::execRemoveItems(const int firstRow, const int nRows) { + const int lastRow = firstRow + nRows - 1; + beginRemoveRows(QModelIndex(), firstRow, lastRow); + m_items.remove(firstRow, nRows); + endRemoveRows(); +} + +void TableModel::execEditItemData(const int row, const QMap& changedValues) { + shared_ptr item = m_items.at(row); + bool isDataChanged = item->setItemData(changedValues); + + if (isDataChanged) { + /// FIXME due to the mapping from roles to the DisplayRole of different columns the complete row + /// is getting notified about (potential) data changes; dataChanged should be called only for + /// the affected columns + const QModelIndex firstIndex = this->index(row, 0); + const QModelIndex lastIndex = this->index(row, ROLE_NAMES.size() - 1); + QList roles = changedValues.keys(); + roles.insert(0, Qt::DisplayRole); + emit dataChanged(firstIndex, lastIndex, roles.toVector()); + } +} + int TableModel::getRoleForColumn(const int column) const { switch (column) { case 0: @@ -154,3 +208,36 @@ int TableModel::getRoleForColumn(const int column) const { break; } } + +QMap TableModel::onlyChangedValues(const QModelIndex& index, + const QMap& roleValueMap) const { + QMap result; + QMap::const_iterator i; + for (i = roleValueMap.constBegin(); i != roleValueMap.constEnd(); ++i) { + const int role = i.key(); + const QVariant newValue = i.value(); + const QVariant oldValue = index.data(role); + // TODO check if role is a editable role? + if (oldValue != newValue) { + bool emptyValueIsEqualToZero = isEmptyValueEqualToZero(role); + if (emptyValueIsEqualToZero && oldValue == QVariant() && newValue == 0) { + qDebug() << "oldValue:" << oldValue << "& newValue:" << newValue + << "mean the same. Ignoring..."; + continue; + } + qDebug() << "oldValue:" << oldValue << "!= newValue:" << newValue; + result.insert(role, newValue); + } else { + qInfo() << "oldValue is already the same as newValue:" << oldValue << "-> ignoring..."; + } + } + return result; +} + +bool TableModel::isEmptyValueEqualToZero(const int role) const { + const QString roleName = ROLE_NAMES.value(role); + if (intColumns.contains(roleName)) { + return true; + } + return false; +} diff --git a/model/tablemodel.h b/model/tablemodel.h index 5778f52..069179e 100644 --- a/model/tablemodel.h +++ b/model/tablemodel.h @@ -3,6 +3,7 @@ #include +class QUndoStack; class ModelItem; using namespace std; @@ -10,11 +11,16 @@ using namespace std; class TableModel : public QAbstractTableModel { Q_OBJECT + friend class InsertRowsCommand; + friend class RemoveRowsCommand; + friend class EditItemCommand; + public: enum UserRoles { NameRole = Qt::UserRole + 1, DescriptionRole, InfoRole, AmountRole, FactorRole }; static QHash ROLE_NAMES; + static QList intColumns; - explicit TableModel(QObject* parent = nullptr); + explicit TableModel(QUndoStack* undoStack, QObject* parent = nullptr); /// QAbstractItemModel interface Qt::ItemFlags flags(const QModelIndex& index) const override; @@ -26,7 +32,7 @@ class TableModel : public QAbstractTableModel { QVariant headerData(int section, Qt::Orientation orientation, int role) const override; bool setData(const QModelIndex& index, const QVariant& value, int role) override; - // bool setItemData(const QModelIndex& index, const QMap& roles) override; + bool setItemData(const QModelIndex& index, const QMap& roles) override; public slots: // bool insertRows(int position, int rows, const QModelIndex& parentIndex = QModelIndex()) @@ -36,11 +42,21 @@ class TableModel : public QAbstractTableModel { void insertItems(int startPosition, const QByteArray& jsonDoc, const QModelIndex& parentIndex); private: - /// members + /// *** members *** QList> m_items; + QUndoStack* m_undoStack; - /// functions + /// *** functions *** + /// undo/redo functions + void execInsertItems(const int firstRow, const QList> valueList); + void execRemoveItems(const int firstRow, const int nRows); + void execEditItemData(const int row, const QMap& changedValues); + + /// misc functions int getRoleForColumn(const int column) const; + QMap onlyChangedValues(const QModelIndex& index, + const QMap& roleValueMap) const; + bool isEmptyValueEqualToZero(const int role) const; }; #endif // TABLEMODEL_H