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 <QRegExp>
Chris@376: #include <QMenu>
Chris@376: #include <QToolBar>
Chris@376: #include <QString>
Chris@376: #include <QTimer>
Chris@376: #include <QAction>
Chris@376: 
Chris@376: #include <iostream>
Chris@376: 
Chris@503: #include <typeinfo>
Chris@503: 
Chris@800: //#define DEBUG_COMMAND_HISTORY 1
Chris@377: 
Chris@376: CommandHistory *CommandHistory::m_instance = 0;
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@376:     m_currentCompound(0),
Chris@376:     m_executeCompound(false),
Chris@376:     m_currentBundle(0),
Chris@502:     m_bundling(false),
Chris@376:     m_bundleTimer(0),
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@376:     m_currentBundle = 0;
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@376:     m_currentCompound = 0;
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: