/* * Olive Community Edition - Non-Linear Video Editor * Copyright (C) 2025 Olive CE Team * * 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::kMaxUndoCommands = 200; class EmptyCommand : public UndoCommand { public: EmptyCommand() { } virtual Project *GetRelevantProject() 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(); UpdateActions(); } 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 (CanRedo()) { 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() > kMaxUndoCommands) { this->beginRemoveRows(QModelIndex(), 0, 0); delete commands_.front().command; commands_.pop_front(); this->endRemoveRows(); } UpdateActions(); } void UndoStack::jump(size_t index) { while (commands_.size() > index) { undo(); } while (commands_.size() < index) { redo(); } } void UndoStack::undo() { if (CanUndo()) { // 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 UpdateActions(); } } void UndoStack::redo() { if (CanRedo()) { // 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 UpdateActions(); } } 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::CanUndo() const { return !commands_.empty() && !dynamic_cast(commands_.back().command); } void UndoStack::UpdateActions() { undo_action_->setEnabled(CanUndo()); redo_action_->setEnabled(CanRedo()); undo_action_->setText( QCoreApplication::translate("UndoStack", "Undo %1") .arg(CanUndo() ? commands_.back().name : QString())); redo_action_->setText( QCoreApplication::translate("UndoStack", "Redo %1") .arg(CanRedo() ? undone_commands_.front().name : QString())); emit indexChanged(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(); } }