Chris@376: /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ Chris@376: Chris@376: /* Chris@376: Sonic Visualiser Chris@376: An audio file viewer and annotation editor. Chris@376: Centre for Digital Music, Queen Mary, University of London. Chris@376: Chris@376: This program is free software; you can redistribute it and/or Chris@376: modify it under the terms of the GNU General Public License as Chris@376: published by the Free Software Foundation; either version 2 of the Chris@376: License, or (at your option) any later version. See the file Chris@376: COPYING included with this distribution for more information. Chris@376: */ Chris@376: Chris@376: /* Chris@376: This is a modified version of a source file from the Rosegarden Chris@376: MIDI and audio sequencer and notation editor, copyright 2000-2006 Chris@376: Chris Cannam, distributed under the GNU General Public License. Chris@376: Chris@376: This file contains traces of the KCommandHistory class from the KDE Chris@376: project, copyright 2000 Werner Trobin and David Faure and Chris@376: distributed under the GNU Lesser General Public License. Chris@376: */ Chris@376: Chris@376: #include "CommandHistory.h" Chris@376: Chris@376: #include "base/Command.h" Chris@376: Chris@961: #include "IconLoader.h" Chris@961: Chris@376: #include Chris@376: #include Chris@376: #include Chris@376: #include Chris@376: #include Chris@376: #include Chris@376: Chris@376: #include Chris@376: Chris@503: #include Chris@503: Chris@800: //#define DEBUG_COMMAND_HISTORY 1 Chris@377: Chris@1408: CommandHistory *CommandHistory::m_instance = nullptr; Chris@376: Chris@376: CommandHistory::CommandHistory() : Chris@376: m_undoLimit(50), Chris@376: m_redoLimit(50), Chris@376: m_menuLimit(15), Chris@376: m_savedAt(0), Chris@1408: m_currentCompound(nullptr), Chris@376: m_executeCompound(false), Chris@1408: m_currentBundle(nullptr), Chris@502: m_bundling(false), Chris@1408: m_bundleTimer(nullptr), Chris@502: m_bundleTimeout(3000) Chris@376: { Chris@961: IconLoader loader; Chris@961: QIcon undoIcon(loader.load("undo")); Chris@961: QIcon redoIcon(loader.load("redo")); Chris@961: Chris@961: m_undoAction = new QAction(undoIcon, ("&Undo"), this); Chris@376: m_undoAction->setShortcut(tr("Ctrl+Z")); Chris@376: m_undoAction->setStatusTip(tr("Undo the last editing operation")); Chris@376: connect(m_undoAction, SIGNAL(triggered()), this, SLOT(undo())); Chris@376: Chris@961: m_undoMenuAction = new QAction(undoIcon, tr("&Undo"), this); Chris@376: connect(m_undoMenuAction, SIGNAL(triggered()), this, SLOT(undo())); Chris@376: Chris@376: m_undoMenu = new QMenu(tr("&Undo")); Chris@376: m_undoMenuAction->setMenu(m_undoMenu); Chris@376: connect(m_undoMenu, SIGNAL(triggered(QAction *)), Chris@1266: this, SLOT(undoActivated(QAction*))); Chris@376: Chris@961: m_redoAction = new QAction(redoIcon, tr("Re&do"), this); Chris@376: m_redoAction->setShortcut(tr("Ctrl+Shift+Z")); Chris@376: m_redoAction->setStatusTip(tr("Redo the last operation that was undone")); Chris@376: connect(m_redoAction, SIGNAL(triggered()), this, SLOT(redo())); Chris@376: Chris@961: m_redoMenuAction = new QAction(redoIcon, tr("Re&do"), this); Chris@376: connect(m_redoMenuAction, SIGNAL(triggered()), this, SLOT(redo())); Chris@376: Chris@376: m_redoMenu = new QMenu(tr("Re&do")); Chris@376: m_redoMenuAction->setMenu(m_redoMenu); Chris@376: connect(m_redoMenu, SIGNAL(triggered(QAction *)), Chris@1266: this, SLOT(redoActivated(QAction*))); Chris@376: } Chris@376: Chris@376: CommandHistory::~CommandHistory() Chris@376: { Chris@376: m_savedAt = -1; Chris@376: clearStack(m_undoStack); Chris@376: clearStack(m_redoStack); Chris@376: Chris@376: delete m_undoMenu; Chris@376: delete m_redoMenu; Chris@376: } Chris@376: Chris@376: CommandHistory * Chris@376: CommandHistory::getInstance() Chris@376: { Chris@376: if (!m_instance) m_instance = new CommandHistory(); Chris@376: return m_instance; Chris@376: } Chris@376: Chris@376: void Chris@376: CommandHistory::clear() Chris@376: { Chris@377: #ifdef DEBUG_COMMAND_HISTORY Chris@752: cerr << "CommandHistory::clear()" << endl; Chris@377: #endif Chris@376: closeBundle(); Chris@376: m_savedAt = -1; Chris@376: clearStack(m_undoStack); Chris@376: clearStack(m_redoStack); Chris@376: updateActions(); Chris@376: } Chris@376: Chris@376: void Chris@376: CommandHistory::registerMenu(QMenu *menu) Chris@376: { Chris@376: menu->addAction(m_undoAction); Chris@376: menu->addAction(m_redoAction); Chris@376: } Chris@376: Chris@376: void Chris@376: CommandHistory::registerToolbar(QToolBar *toolbar) Chris@376: { Chris@376: toolbar->addAction(m_undoMenuAction); Chris@376: toolbar->addAction(m_redoMenuAction); Chris@376: } Chris@376: Chris@376: void Chris@376: CommandHistory::addCommand(Command *command) Chris@376: { Chris@376: if (!command) return; Chris@376: Chris@376: if (m_currentCompound) { Chris@1266: addToCompound(command, m_executeCompound); Chris@1266: return; Chris@376: } Chris@376: Chris@376: addCommand(command, true); Chris@376: } Chris@376: Chris@376: void Chris@376: CommandHistory::addCommand(Command *command, bool execute, bool bundle) Chris@376: { Chris@376: if (!command) return; Chris@376: Chris@397: #ifdef DEBUG_COMMAND_HISTORY Chris@752: cerr << "CommandHistory::addCommand: " << command->getName() << " of type " << typeid(*command).name() << " at " << command << ": execute = " << execute << ", bundle = " << bundle << " (m_currentCompound = " << m_currentCompound << ", m_currentBundle = " << m_currentBundle << ")" << endl; Chris@397: #endif Chris@397: Chris@376: if (m_currentCompound) { Chris@1266: addToCompound(command, execute); Chris@1266: return; Chris@376: } Chris@376: Chris@376: if (bundle) { Chris@1266: addToBundle(command, execute); Chris@1266: return; Chris@376: } else if (m_currentBundle) { Chris@1266: closeBundle(); Chris@376: } Chris@376: Chris@377: #ifdef DEBUG_COMMAND_HISTORY Chris@377: if (!m_redoStack.empty()) { Chris@752: cerr << "CommandHistory::clearing redo stack" << endl; Chris@377: } Chris@377: #endif Chris@376: Chris@376: // We can't redo after adding a command Chris@376: clearStack(m_redoStack); Chris@376: Chris@376: // can we reach savedAt? Chris@376: if ((int)m_undoStack.size() < m_savedAt) m_savedAt = -1; // nope Chris@376: Chris@376: m_undoStack.push(command); Chris@376: clipCommands(); Chris@376: Chris@376: if (execute) { Chris@1266: command->execute(); Chris@376: } Chris@376: Chris@376: // Emit even if we aren't executing the command, because Chris@376: // someone must have executed it for this to make any sense Chris@376: emit commandExecuted(); Chris@376: emit commandExecuted(command); Chris@502: if (!m_bundling) emit activity(command->getName()); Chris@502: Chris@376: updateActions(); Chris@376: } Chris@376: Chris@376: void Chris@376: CommandHistory::addToBundle(Command *command, bool execute) Chris@376: { Chris@376: if (m_currentBundle) { Chris@1266: if (!command || (command->getName() != m_currentBundleName)) { Chris@377: #ifdef DEBUG_COMMAND_HISTORY Chris@752: cerr << "CommandHistory::addToBundle: " << command->getName() Chris@752: << ": closing current bundle" << endl; Chris@377: #endif Chris@1266: closeBundle(); Chris@1266: } Chris@376: } Chris@376: Chris@376: if (!command) return; Chris@376: Chris@376: if (!m_currentBundle) { Chris@377: Chris@377: #ifdef DEBUG_COMMAND_HISTORY Chris@752: cerr << "CommandHistory::addToBundle: " << command->getName() Chris@752: << ": creating new bundle" << endl; Chris@377: #endif Chris@377: Chris@1266: // need to addCommand before setting m_currentBundle, as addCommand Chris@1266: // with bundle false will reset m_currentBundle to 0 Chris@1266: MacroCommand *mc = new BundleCommand(command->getName()); Chris@502: m_bundling = true; Chris@1266: addCommand(mc, false); Chris@502: m_bundling = false; Chris@1266: m_currentBundle = mc; Chris@1266: m_currentBundleName = command->getName(); Chris@376: } Chris@376: Chris@377: #ifdef DEBUG_COMMAND_HISTORY Chris@752: cerr << "CommandHistory::addToBundle: " << command->getName() Chris@752: << ": adding to bundle" << endl; Chris@377: #endif Chris@377: Chris@376: if (execute) command->execute(); Chris@376: m_currentBundle->addCommand(command); Chris@376: Chris@377: // Emit even if we aren't executing the command, because Chris@377: // someone must have executed it for this to make any sense Chris@377: emit commandExecuted(); Chris@377: emit commandExecuted(command); Chris@377: Chris@377: updateActions(); Chris@377: Chris@376: delete m_bundleTimer; Chris@376: m_bundleTimer = new QTimer(this); Chris@376: connect(m_bundleTimer, SIGNAL(timeout()), this, SLOT(bundleTimerTimeout())); Chris@376: m_bundleTimer->start(m_bundleTimeout); Chris@376: } Chris@376: Chris@376: void Chris@376: CommandHistory::closeBundle() Chris@376: { Chris@752: if (m_currentBundle) { Chris@377: #ifdef DEBUG_COMMAND_HISTORY Chris@752: cerr << "CommandHistory::closeBundle" << endl; Chris@377: #endif Chris@752: emit activity(m_currentBundle->getName()); Chris@752: } Chris@1408: m_currentBundle = nullptr; Chris@376: m_currentBundleName = ""; Chris@376: } Chris@376: Chris@376: void Chris@376: CommandHistory::bundleTimerTimeout() Chris@376: { Chris@377: #ifdef DEBUG_COMMAND_HISTORY Chris@752: cerr << "CommandHistory::bundleTimerTimeout: bundle is " << m_currentBundle << endl; Chris@377: #endif Chris@377: Chris@376: closeBundle(); Chris@376: } Chris@376: Chris@376: void Chris@376: CommandHistory::addToCompound(Command *command, bool execute) Chris@376: { Chris@376: if (!m_currentCompound) { Chris@1266: cerr << "CommandHistory::addToCompound: ERROR: no compound operation in progress!" << endl; Chris@376: return; Chris@376: } Chris@376: Chris@752: #ifdef DEBUG_COMMAND_HISTORY Chris@752: cerr << "CommandHistory::addToCompound[" << m_currentCompound->getName() << "]: " << command->getName() << " (exec: " << execute << ")" << endl; Chris@752: #endif Chris@752: Chris@376: if (execute) command->execute(); Chris@376: m_currentCompound->addCommand(command); Chris@376: } Chris@376: Chris@376: void Chris@376: CommandHistory::startCompoundOperation(QString name, bool execute) Chris@376: { Chris@376: if (m_currentCompound) { Chris@1266: cerr << "CommandHistory::startCompoundOperation: ERROR: compound operation already in progress!" << endl; Chris@1266: cerr << "(name is " << m_currentCompound->getName() << ")" << endl; Chris@376: return; Chris@376: } Chris@376: Chris@752: #ifdef DEBUG_COMMAND_HISTORY Chris@752: cerr << "CommandHistory::startCompoundOperation: " << name << " (exec: " << execute << ")" << endl; Chris@752: #endif Chris@752: Chris@376: closeBundle(); Chris@752: Chris@376: m_currentCompound = new MacroCommand(name); Chris@376: m_executeCompound = execute; Chris@376: } Chris@376: Chris@376: void Chris@376: CommandHistory::endCompoundOperation() Chris@376: { Chris@376: if (!m_currentCompound) { Chris@1266: cerr << "CommandHistory::endCompoundOperation: ERROR: no compound operation in progress!" << endl; Chris@376: return; Chris@376: } Chris@752: Chris@752: #ifdef DEBUG_COMMAND_HISTORY Chris@752: cerr << "CommandHistory::endCompoundOperation: " << m_currentCompound->getName() << endl; Chris@752: #endif Chris@376: Chris@376: MacroCommand *toAdd = m_currentCompound; Chris@1408: m_currentCompound = nullptr; Chris@376: Chris@376: if (toAdd->haveCommands()) { Chris@376: Chris@376: // We don't execute the macro command here, because we have Chris@376: // been executing the individual commands as we went along if Chris@376: // m_executeCompound was true. Chris@376: addCommand(toAdd, false); Chris@376: } Chris@376: } Chris@376: Chris@376: void Chris@376: CommandHistory::addExecutedCommand(Command *command) Chris@376: { Chris@376: addCommand(command, false); Chris@376: } Chris@376: Chris@376: void Chris@376: CommandHistory::addCommandAndExecute(Command *command) Chris@376: { Chris@376: addCommand(command, true); Chris@376: } Chris@376: Chris@376: void Chris@376: CommandHistory::undo() Chris@376: { Chris@376: if (m_undoStack.empty()) return; Chris@376: Chris@377: #ifdef DEBUG_COMMAND_HISTORY Chris@752: cerr << "CommandHistory::undo()" << endl; Chris@377: #endif Chris@377: Chris@376: closeBundle(); Chris@376: Chris@376: Command *command = m_undoStack.top(); Chris@376: command->unexecute(); Chris@376: emit commandExecuted(); Chris@376: emit commandUnexecuted(command); Chris@502: emit activity(tr("Undo %1").arg(command->getName())); Chris@376: Chris@376: m_redoStack.push(command); Chris@376: m_undoStack.pop(); Chris@376: Chris@376: clipCommands(); Chris@376: updateActions(); Chris@376: Chris@376: if ((int)m_undoStack.size() == m_savedAt) emit documentRestored(); Chris@376: } Chris@376: Chris@376: void Chris@376: CommandHistory::redo() Chris@376: { Chris@376: if (m_redoStack.empty()) return; Chris@376: Chris@377: #ifdef DEBUG_COMMAND_HISTORY Chris@752: cerr << "CommandHistory::redo()" << endl; Chris@377: #endif Chris@377: Chris@376: closeBundle(); Chris@376: Chris@376: Command *command = m_redoStack.top(); Chris@376: command->execute(); Chris@376: emit commandExecuted(); Chris@376: emit commandExecuted(command); Chris@502: emit activity(tr("Redo %1").arg(command->getName())); Chris@376: Chris@376: m_undoStack.push(command); Chris@376: m_redoStack.pop(); Chris@376: // no need to clip Chris@376: Chris@376: updateActions(); Chris@376: Chris@376: if ((int)m_undoStack.size() == m_savedAt) emit documentRestored(); Chris@376: } Chris@376: Chris@376: void Chris@376: CommandHistory::setUndoLimit(int limit) Chris@376: { Chris@376: if (limit > 0 && limit != m_undoLimit) { Chris@376: m_undoLimit = limit; Chris@376: clipCommands(); Chris@376: } Chris@376: } Chris@376: Chris@376: void Chris@376: CommandHistory::setRedoLimit(int limit) Chris@376: { Chris@376: if (limit > 0 && limit != m_redoLimit) { Chris@376: m_redoLimit = limit; Chris@376: clipCommands(); Chris@376: } Chris@376: } Chris@376: Chris@376: void Chris@376: CommandHistory::setMenuLimit(int limit) Chris@376: { Chris@376: m_menuLimit = limit; Chris@376: updateActions(); Chris@376: } Chris@376: Chris@376: void Chris@376: CommandHistory::setBundleTimeout(int ms) Chris@376: { Chris@376: m_bundleTimeout = ms; Chris@376: } Chris@376: Chris@376: void Chris@376: CommandHistory::documentSaved() Chris@376: { Chris@376: closeBundle(); Chris@908: m_savedAt = int(m_undoStack.size()); Chris@376: } Chris@376: Chris@376: void Chris@376: CommandHistory::clipCommands() Chris@376: { Chris@908: if (int(m_undoStack.size()) > m_undoLimit) { Chris@1266: m_savedAt -= (int(m_undoStack.size()) - m_undoLimit); Chris@376: } Chris@376: Chris@376: clipStack(m_undoStack, m_undoLimit); Chris@376: clipStack(m_redoStack, m_redoLimit); Chris@376: } Chris@376: Chris@376: void Chris@376: CommandHistory::clipStack(CommandStack &stack, int limit) Chris@376: { Chris@376: int i; Chris@376: Chris@376: if ((int)stack.size() > limit) { Chris@376: Chris@1266: CommandStack tempStack; Chris@376: Chris@1266: for (i = 0; i < limit; ++i) { Chris@377: #ifdef DEBUG_COMMAND_HISTORY Chris@1266: Command *command = stack.top(); Chris@1266: cerr << "CommandHistory::clipStack: Saving recent command: " << command->getName() << " at " << command << endl; Chris@377: #endif Chris@1266: tempStack.push(stack.top()); Chris@1266: stack.pop(); Chris@1266: } Chris@376: Chris@1266: clearStack(stack); Chris@376: Chris@1266: for (i = 0; i < m_undoLimit; ++i) { Chris@1266: stack.push(tempStack.top()); Chris@1266: tempStack.pop(); Chris@1266: } Chris@376: } Chris@376: } Chris@376: Chris@376: void Chris@376: CommandHistory::clearStack(CommandStack &stack) Chris@376: { Chris@376: while (!stack.empty()) { Chris@1266: Command *command = stack.top(); Chris@1266: // Not safe to call getName() on a command about to be deleted Chris@377: #ifdef DEBUG_COMMAND_HISTORY Chris@1266: cerr << "CommandHistory::clearStack: About to delete command " << command << endl; Chris@377: #endif Chris@1266: delete command; Chris@1266: stack.pop(); Chris@376: } Chris@376: } Chris@376: Chris@376: void Chris@376: CommandHistory::undoActivated(QAction *action) Chris@376: { Chris@376: int pos = m_actionCounts[action]; Chris@376: for (int i = 0; i <= pos; ++i) { Chris@1266: undo(); Chris@376: } Chris@376: } Chris@376: Chris@376: void Chris@376: CommandHistory::redoActivated(QAction *action) Chris@376: { Chris@376: int pos = m_actionCounts[action]; Chris@376: for (int i = 0; i <= pos; ++i) { Chris@1266: redo(); Chris@376: } Chris@376: } Chris@376: Chris@376: void Chris@376: CommandHistory::updateActions() Chris@376: { Chris@376: m_actionCounts.clear(); Chris@376: Chris@376: for (int undo = 0; undo <= 1; ++undo) { Chris@376: Chris@1266: QAction *action(undo ? m_undoAction : m_redoAction); Chris@1266: QAction *menuAction(undo ? m_undoMenuAction : m_redoMenuAction); Chris@1266: QMenu *menu(undo ? m_undoMenu : m_redoMenu); Chris@1266: CommandStack &stack(undo ? m_undoStack : m_redoStack); Chris@376: Chris@1266: if (stack.empty()) { Chris@376: Chris@1266: QString text(undo ? tr("Nothing to undo") : tr("Nothing to redo")); Chris@376: Chris@1266: action->setEnabled(false); Chris@1266: action->setText(text); Chris@376: Chris@1266: menuAction->setEnabled(false); Chris@1266: menuAction->setText(text); Chris@376: Chris@1266: } else { Chris@376: Chris@1266: action->setEnabled(true); Chris@1266: menuAction->setEnabled(true); Chris@376: Chris@1266: QString commandName = stack.top()->getName(); Chris@1266: commandName.replace(QRegExp("&"), ""); Chris@376: Chris@1266: QString text = (undo ? tr("&Undo %1") : tr("Re&do %1")) Chris@1266: .arg(commandName); Chris@376: Chris@1266: action->setText(text); Chris@1266: menuAction->setText(text); Chris@1266: } Chris@376: Chris@1266: menu->clear(); Chris@376: Chris@1266: CommandStack tempStack; Chris@1266: int j = 0; Chris@376: Chris@1266: while (j < m_menuLimit && !stack.empty()) { Chris@376: Chris@1266: Command *command = stack.top(); Chris@1266: tempStack.push(command); Chris@1266: stack.pop(); Chris@376: Chris@1266: QString commandName = command->getName(); Chris@1266: commandName.replace(QRegExp("&"), ""); Chris@376: Chris@1266: QString text; Chris@1266: if (undo) text = tr("&Undo %1").arg(commandName); Chris@1266: else text = tr("Re&do %1").arg(commandName); Chris@1266: Chris@1266: QAction *action = menu->addAction(text); Chris@1266: m_actionCounts[action] = j++; Chris@1266: } Chris@376: Chris@1266: while (!tempStack.empty()) { Chris@1266: stack.push(tempStack.top()); Chris@1266: tempStack.pop(); Chris@1266: } Chris@376: } Chris@376: } Chris@376: