/*** Olive - Non-Linear Video Editor Copyright (C) 2022 Olive Team Modifications Copyright (C) 2025 mikesolar This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ***/ #include "undostack.h" #include namespace olive { const int UndoStack::k_max_undo_commands = 200; class EmptyCommand : public UndoCommand { public: EmptyCommand() { } virtual Project *get_relevant_project() const override { return nullptr; } protected: virtual void redo() override { } virtual void undo() override { } }; UndoStack::UndoStack() { undo_action_ = new QAction(); connect(undo_action_, &QAction::triggered, this, &UndoStack::undo); redo_action_ = new QAction(); connect(redo_action_, &QAction::triggered, this, &UndoStack::redo); clear(); update_actions(); } UndoStack::~UndoStack() { clear(); delete undo_action_; delete redo_action_; } void UndoStack::push(UndoCommand *command, const QString &name) { MultiUndoCommand *mcu = dynamic_cast(command); if (mcu && mcu->child_count() == 0) { delete command; return; } // Clear any redoable commands this->beginRemoveRows(QModelIndex(), commands_.size(), commands_.size() + undone_commands_.size()); if (can_redo()) { for (auto it = undone_commands_.cbegin(); it != undone_commands_.cend(); it++) { delete (*it).command; } undone_commands_.clear(); } this->endRemoveRows(); // Do command and push this->beginInsertRows(QModelIndex(), commands_.size(), commands_.size()); command->redo_and_set_modified(); commands_.push_back({ command, name }); this->endInsertRows(); // Delete oldest if (commands_.size() > k_max_undo_commands) { this->beginRemoveRows(QModelIndex(), 0, 0); delete commands_.front().command; commands_.pop_front(); this->endRemoveRows(); } update_actions(); } void UndoStack::push_pre_executed(UndoCommand *command, const QString &name) { MultiUndoCommand *mcu = dynamic_cast(command); if (mcu && mcu->child_count() == 0) { delete command; return; } // Clear any redoable commands this->beginRemoveRows(QModelIndex(), commands_.size(), commands_.size() + undone_commands_.size()); if (can_redo()) { for (auto it = undone_commands_.cbegin(); it != undone_commands_.cend(); it++) { delete (*it).command; } undone_commands_.clear(); } this->endRemoveRows(); // Push without redoing: the caller already executed the children. this->beginInsertRows(QModelIndex(), commands_.size(), commands_.size()); commands_.push_back({ command, name }); this->endInsertRows(); // Delete oldest if (commands_.size() > k_max_undo_commands) { this->beginRemoveRows(QModelIndex(), 0, 0); delete commands_.front().command; commands_.pop_front(); this->endRemoveRows(); } update_actions(); } void UndoStack::jump(size_t index) { while (commands_.size() > index) { undo(); } while (commands_.size() < index) { redo(); } } void UndoStack::undo() { if (can_undo()) { // Undo most recently done command commands_.back().command->undo_and_set_modified(); // Place at the front of the "undone commands" list undone_commands_.push_front(commands_.back()); // Remove undone command from the commands list commands_.pop_back(); // Update actions update_actions(); } } void UndoStack::redo() { if (can_redo()) { // Redo most recently undone command undone_commands_.front().command->redo_and_set_modified(); // Place at the back of the done commands list commands_.push_back(undone_commands_.front()); // Remove done command from undone list undone_commands_.pop_front(); // Update actions update_actions(); } } void UndoStack::clear() { this->beginResetModel(); for (auto it = commands_.cbegin(); it != commands_.cend(); it++) { delete (*it).command; } commands_.clear(); for (auto it = undone_commands_.cbegin(); it != undone_commands_.cend(); it++) { delete (*it).command; } undone_commands_.clear(); this->endResetModel(); push(new EmptyCommand(), tr("New/Open Project")); } bool UndoStack::can_undo() const { return !commands_.empty() && !dynamic_cast(commands_.back().command); } void UndoStack::update_actions() { undo_action_->setEnabled(can_undo()); redo_action_->setEnabled(can_redo()); undo_action_->setText( QCoreApplication::translate("UndoStack", "Undo %1") .arg(can_undo() ? commands_.back().name : QString())); redo_action_->setText( QCoreApplication::translate("UndoStack", "Redo %1") .arg(can_redo() ? undone_commands_.front().name : QString())); emit index_changed(commands_.size()); } int UndoStack::columnCount(const QModelIndex &parent) const { if (parent.isValid()) { return 0; } return 2; } QVariant UndoStack::data(const QModelIndex &index, int role) const { if (role == Qt::DisplayRole) { switch (index.column()) { case 0: return index.row() + 1; case 1: { std::list::const_iterator it; size_t real_index = index.row(); if (real_index < commands_.size()) { it = commands_.cbegin(); } else { real_index -= commands_.size(); it = undone_commands_.cbegin(); } for (size_t i = 0; i < real_index; i++) { it++; } const QString &name = (*it).name; return (name.isEmpty()) ? tr("Command") : name; } } } else if (role == Qt::ForegroundRole) { if (size_t(index.row()) >= commands_.size()) { return QVariant(QColor(Qt::gray)); } } return QVariant(); } QModelIndex UndoStack::index(int row, int column, const QModelIndex &parent) const { return createIndex(row, column, nullptr); } QModelIndex UndoStack::parent(const QModelIndex &index) const { return QModelIndex(); } int UndoStack::rowCount(const QModelIndex &parent) const { if (parent.isValid()) { return 0; } return commands_.size() + undone_commands_.size(); } QVariant UndoStack::headerData(int section, Qt::Orientation orientation, int role) const { if (role == Qt::DisplayRole) { switch (section) { case 0: return QStringLiteral("Number"); case 1: return QStringLiteral("Action"); } } return QVariant(); } bool UndoStack::hasChildren(const QModelIndex &parent) const { return !parent.isValid(); } }