svgui  1.9
CommandHistory.cpp
Go to the documentation of this file.
00001 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
00002 
00003 /*
00004     Sonic Visualiser
00005     An audio file viewer and annotation editor.
00006     Centre for Digital Music, Queen Mary, University of London.
00007     
00008     This program is free software; you can redistribute it and/or
00009     modify it under the terms of the GNU General Public License as
00010     published by the Free Software Foundation; either version 2 of the
00011     License, or (at your option) any later version.  See the file
00012     COPYING included with this distribution for more information.
00013 */
00014 
00015 /*
00016    This is a modified version of a source file from the Rosegarden
00017    MIDI and audio sequencer and notation editor, copyright 2000-2006
00018    Chris Cannam, distributed under the GNU General Public License.
00019 
00020    This file contains traces of the KCommandHistory class from the KDE
00021    project, copyright 2000 Werner Trobin and David Faure and
00022    distributed under the GNU Lesser General Public License.
00023 */
00024 
00025 #include "CommandHistory.h"
00026 
00027 #include "base/Command.h"
00028 
00029 #include <QRegExp>
00030 #include <QMenu>
00031 #include <QToolBar>
00032 #include <QString>
00033 #include <QTimer>
00034 #include <QAction>
00035 
00036 #include <iostream>
00037 
00038 #include <typeinfo>
00039 
00040 //#define DEBUG_COMMAND_HISTORY 1
00041 
00042 CommandHistory *CommandHistory::m_instance = 0;
00043 
00044 CommandHistory::CommandHistory() :
00045     m_undoLimit(50),
00046     m_redoLimit(50),
00047     m_menuLimit(15),
00048     m_savedAt(0),
00049     m_currentCompound(0),
00050     m_executeCompound(false),
00051     m_currentBundle(0),
00052     m_bundling(false),
00053     m_bundleTimer(0),
00054     m_bundleTimeout(3000)
00055 {
00056     m_undoAction = new QAction(QIcon(":/icons/undo.png"), tr("&Undo"), this);
00057     m_undoAction->setShortcut(tr("Ctrl+Z"));
00058     m_undoAction->setStatusTip(tr("Undo the last editing operation"));
00059     connect(m_undoAction, SIGNAL(triggered()), this, SLOT(undo()));
00060     
00061     m_undoMenuAction = new QAction(QIcon(":/icons/undo.png"), tr("&Undo"), this);
00062     connect(m_undoMenuAction, SIGNAL(triggered()), this, SLOT(undo()));
00063     
00064     m_undoMenu = new QMenu(tr("&Undo"));
00065     m_undoMenuAction->setMenu(m_undoMenu);
00066     connect(m_undoMenu, SIGNAL(triggered(QAction *)),
00067             this, SLOT(undoActivated(QAction*)));
00068 
00069     m_redoAction = new QAction(QIcon(":/icons/redo.png"), tr("Re&do"), this);
00070     m_redoAction->setShortcut(tr("Ctrl+Shift+Z"));
00071     m_redoAction->setStatusTip(tr("Redo the last operation that was undone"));
00072     connect(m_redoAction, SIGNAL(triggered()), this, SLOT(redo()));
00073     
00074     m_redoMenuAction = new QAction(QIcon(":/icons/redo.png"), tr("Re&do"), this);
00075     connect(m_redoMenuAction, SIGNAL(triggered()), this, SLOT(redo()));
00076 
00077     m_redoMenu = new QMenu(tr("Re&do"));
00078     m_redoMenuAction->setMenu(m_redoMenu);
00079     connect(m_redoMenu, SIGNAL(triggered(QAction *)),
00080             this, SLOT(redoActivated(QAction*)));
00081 }
00082 
00083 CommandHistory::~CommandHistory()
00084 {
00085     m_savedAt = -1;
00086     clearStack(m_undoStack);
00087     clearStack(m_redoStack);
00088 
00089     delete m_undoMenu;
00090     delete m_redoMenu;
00091 }
00092 
00093 CommandHistory *
00094 CommandHistory::getInstance()
00095 {
00096     if (!m_instance) m_instance = new CommandHistory();
00097     return m_instance;
00098 }
00099 
00100 void
00101 CommandHistory::clear()
00102 {
00103 #ifdef DEBUG_COMMAND_HISTORY
00104     cerr << "CommandHistory::clear()" << endl;
00105 #endif
00106     closeBundle();
00107     m_savedAt = -1;
00108     clearStack(m_undoStack);
00109     clearStack(m_redoStack);
00110     updateActions();
00111 }
00112 
00113 void
00114 CommandHistory::registerMenu(QMenu *menu)
00115 {
00116     menu->addAction(m_undoAction);
00117     menu->addAction(m_redoAction);
00118 }
00119 
00120 void
00121 CommandHistory::registerToolbar(QToolBar *toolbar)
00122 {
00123     toolbar->addAction(m_undoMenuAction);
00124     toolbar->addAction(m_redoMenuAction);
00125 }
00126 
00127 void
00128 CommandHistory::addCommand(Command *command)
00129 {
00130     if (!command) return;
00131 
00132     if (m_currentCompound) {
00133         addToCompound(command, m_executeCompound);
00134         return;
00135     }
00136 
00137     addCommand(command, true);
00138 }
00139 
00140 void
00141 CommandHistory::addCommand(Command *command, bool execute, bool bundle)
00142 {
00143     if (!command) return;
00144 
00145 #ifdef DEBUG_COMMAND_HISTORY
00146     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;
00147 #endif
00148 
00149     if (m_currentCompound) {
00150         addToCompound(command, execute);
00151         return;
00152     }
00153 
00154     if (bundle) {
00155         addToBundle(command, execute);
00156         return;
00157     } else if (m_currentBundle) {
00158         closeBundle();
00159     }
00160 
00161 #ifdef DEBUG_COMMAND_HISTORY
00162     if (!m_redoStack.empty()) {
00163         cerr << "CommandHistory::clearing redo stack" << endl;
00164     }
00165 #endif
00166 
00167     // We can't redo after adding a command
00168     clearStack(m_redoStack);
00169 
00170     // can we reach savedAt?
00171     if ((int)m_undoStack.size() < m_savedAt) m_savedAt = -1; // nope
00172 
00173     m_undoStack.push(command);
00174     clipCommands();
00175     
00176     if (execute) {
00177         command->execute();
00178     }
00179 
00180     // Emit even if we aren't executing the command, because
00181     // someone must have executed it for this to make any sense
00182     emit commandExecuted();
00183     emit commandExecuted(command);
00184     if (!m_bundling) emit activity(command->getName());
00185     
00186     updateActions();
00187 }
00188 
00189 void
00190 CommandHistory::addToBundle(Command *command, bool execute)
00191 {
00192     if (m_currentBundle) {
00193         if (!command || (command->getName() != m_currentBundleName)) {
00194 #ifdef DEBUG_COMMAND_HISTORY
00195             cerr << "CommandHistory::addToBundle: " << command->getName()
00196                  << ": closing current bundle" << endl;
00197 #endif
00198             closeBundle();
00199         }
00200     }
00201 
00202     if (!command) return;
00203 
00204     if (!m_currentBundle) {
00205 
00206 #ifdef DEBUG_COMMAND_HISTORY
00207         cerr << "CommandHistory::addToBundle: " << command->getName()
00208              << ": creating new bundle" << endl;
00209 #endif
00210 
00211         // need to addCommand before setting m_currentBundle, as addCommand
00212         // with bundle false will reset m_currentBundle to 0
00213         MacroCommand *mc = new BundleCommand(command->getName());
00214         m_bundling = true;
00215         addCommand(mc, false);
00216         m_bundling = false;
00217         m_currentBundle = mc;
00218         m_currentBundleName = command->getName();
00219     }
00220 
00221 #ifdef DEBUG_COMMAND_HISTORY
00222     cerr << "CommandHistory::addToBundle: " << command->getName()
00223          << ": adding to bundle" << endl;
00224 #endif
00225 
00226     if (execute) command->execute();
00227     m_currentBundle->addCommand(command);
00228 
00229     // Emit even if we aren't executing the command, because
00230     // someone must have executed it for this to make any sense
00231     emit commandExecuted();
00232     emit commandExecuted(command);
00233 
00234     updateActions();
00235 
00236     delete m_bundleTimer;
00237     m_bundleTimer = new QTimer(this);
00238     connect(m_bundleTimer, SIGNAL(timeout()), this, SLOT(bundleTimerTimeout()));
00239     m_bundleTimer->start(m_bundleTimeout);
00240 }
00241 
00242 void
00243 CommandHistory::closeBundle()
00244 {
00245     if (m_currentBundle) {
00246 #ifdef DEBUG_COMMAND_HISTORY
00247         cerr << "CommandHistory::closeBundle" << endl;
00248 #endif
00249         emit activity(m_currentBundle->getName());
00250     }
00251     m_currentBundle = 0;
00252     m_currentBundleName = "";
00253 }
00254 
00255 void
00256 CommandHistory::bundleTimerTimeout()
00257 {
00258 #ifdef DEBUG_COMMAND_HISTORY
00259     cerr << "CommandHistory::bundleTimerTimeout: bundle is " << m_currentBundle << endl;
00260 #endif
00261 
00262     closeBundle();
00263 }
00264 
00265 void
00266 CommandHistory::addToCompound(Command *command, bool execute)
00267 {
00268     if (!m_currentCompound) {
00269         cerr << "CommandHistory::addToCompound: ERROR: no compound operation in progress!" << endl;
00270         return;
00271     }
00272 
00273 #ifdef DEBUG_COMMAND_HISTORY
00274     cerr << "CommandHistory::addToCompound[" << m_currentCompound->getName() << "]: " << command->getName() << " (exec: " << execute << ")" << endl;
00275 #endif
00276 
00277     if (execute) command->execute();
00278     m_currentCompound->addCommand(command);
00279 }
00280 
00281 void
00282 CommandHistory::startCompoundOperation(QString name, bool execute)
00283 {
00284     if (m_currentCompound) {
00285         cerr << "CommandHistory::startCompoundOperation: ERROR: compound operation already in progress!" << endl;
00286         cerr << "(name is " << m_currentCompound->getName() << ")" << endl;
00287         return;
00288     }
00289  
00290 #ifdef DEBUG_COMMAND_HISTORY
00291     cerr << "CommandHistory::startCompoundOperation: " << name << " (exec: " << execute << ")" << endl;
00292 #endif
00293    
00294     closeBundle();
00295 
00296     m_currentCompound = new MacroCommand(name);
00297     m_executeCompound = execute;
00298 }
00299 
00300 void
00301 CommandHistory::endCompoundOperation()
00302 {
00303     if (!m_currentCompound) {
00304         cerr << "CommandHistory::endCompoundOperation: ERROR: no compound operation in progress!" << endl;
00305         return;
00306     }
00307  
00308 #ifdef DEBUG_COMMAND_HISTORY
00309     cerr << "CommandHistory::endCompoundOperation: " << m_currentCompound->getName() << endl;
00310 #endif
00311 
00312     MacroCommand *toAdd = m_currentCompound;
00313     m_currentCompound = 0;
00314 
00315     if (toAdd->haveCommands()) {
00316 
00317         // We don't execute the macro command here, because we have
00318         // been executing the individual commands as we went along if
00319         // m_executeCompound was true.
00320         addCommand(toAdd, false);
00321     }
00322 }    
00323 
00324 void
00325 CommandHistory::addExecutedCommand(Command *command)
00326 {
00327     addCommand(command, false);
00328 }
00329 
00330 void
00331 CommandHistory::addCommandAndExecute(Command *command)
00332 {
00333     addCommand(command, true);
00334 }
00335 
00336 void
00337 CommandHistory::undo()
00338 {
00339     if (m_undoStack.empty()) return;
00340 
00341 #ifdef DEBUG_COMMAND_HISTORY
00342     cerr << "CommandHistory::undo()" << endl;
00343 #endif
00344 
00345     closeBundle();
00346 
00347     Command *command = m_undoStack.top();
00348     command->unexecute();
00349     emit commandExecuted();
00350     emit commandUnexecuted(command);
00351     emit activity(tr("Undo %1").arg(command->getName()));
00352 
00353     m_redoStack.push(command);
00354     m_undoStack.pop();
00355 
00356     clipCommands();
00357     updateActions();
00358 
00359     if ((int)m_undoStack.size() == m_savedAt) emit documentRestored();
00360 }
00361 
00362 void
00363 CommandHistory::redo()
00364 {
00365     if (m_redoStack.empty()) return;
00366 
00367 #ifdef DEBUG_COMMAND_HISTORY
00368     cerr << "CommandHistory::redo()" << endl;
00369 #endif
00370 
00371     closeBundle();
00372 
00373     Command *command = m_redoStack.top();
00374     command->execute();
00375     emit commandExecuted();
00376     emit commandExecuted(command);
00377     emit activity(tr("Redo %1").arg(command->getName()));
00378 
00379     m_undoStack.push(command);
00380     m_redoStack.pop();
00381     // no need to clip
00382 
00383     updateActions();
00384 
00385     if ((int)m_undoStack.size() == m_savedAt) emit documentRestored();
00386 }
00387 
00388 void
00389 CommandHistory::setUndoLimit(int limit)
00390 {
00391     if (limit > 0 && limit != m_undoLimit) {
00392         m_undoLimit = limit;
00393         clipCommands();
00394     }
00395 }
00396 
00397 void
00398 CommandHistory::setRedoLimit(int limit)
00399 {
00400     if (limit > 0 && limit != m_redoLimit) {
00401         m_redoLimit = limit;
00402         clipCommands();
00403     }
00404 }
00405 
00406 void
00407 CommandHistory::setMenuLimit(int limit)
00408 {
00409     m_menuLimit = limit;
00410     updateActions();
00411 }
00412 
00413 void
00414 CommandHistory::setBundleTimeout(int ms)
00415 {
00416     m_bundleTimeout = ms;
00417 }
00418 
00419 void
00420 CommandHistory::documentSaved()
00421 {
00422     closeBundle();
00423     m_savedAt = m_undoStack.size();
00424 }
00425 
00426 void
00427 CommandHistory::clipCommands()
00428 {
00429     if ((int)m_undoStack.size() > m_undoLimit) {
00430         m_savedAt -= (m_undoStack.size() - m_undoLimit);
00431     }
00432 
00433     clipStack(m_undoStack, m_undoLimit);
00434     clipStack(m_redoStack, m_redoLimit);
00435 }
00436 
00437 void
00438 CommandHistory::clipStack(CommandStack &stack, int limit)
00439 {
00440     int i;
00441 
00442     if ((int)stack.size() > limit) {
00443 
00444         CommandStack tempStack;
00445 
00446         for (i = 0; i < limit; ++i) {
00447 #ifdef DEBUG_COMMAND_HISTORY
00448             Command *command = stack.top();
00449             cerr << "CommandHistory::clipStack: Saving recent command: " << command->getName() << " at " << command << endl;
00450 #endif
00451             tempStack.push(stack.top());
00452             stack.pop();
00453         }
00454 
00455         clearStack(stack);
00456 
00457         for (i = 0; i < m_undoLimit; ++i) {
00458             stack.push(tempStack.top());
00459             tempStack.pop();
00460         }
00461     }
00462 }
00463 
00464 void
00465 CommandHistory::clearStack(CommandStack &stack)
00466 {
00467     while (!stack.empty()) {
00468         Command *command = stack.top();
00469         // Not safe to call getName() on a command about to be deleted
00470 #ifdef DEBUG_COMMAND_HISTORY
00471         cerr << "CommandHistory::clearStack: About to delete command " << command << endl;
00472 #endif
00473         delete command;
00474         stack.pop();
00475     }
00476 }
00477 
00478 void
00479 CommandHistory::undoActivated(QAction *action)
00480 {
00481     int pos = m_actionCounts[action];
00482     for (int i = 0; i <= pos; ++i) {
00483         undo();
00484     }
00485 }
00486 
00487 void
00488 CommandHistory::redoActivated(QAction *action)
00489 {
00490     int pos = m_actionCounts[action];
00491     for (int i = 0; i <= pos; ++i) {
00492         redo();
00493     }
00494 }
00495 
00496 void
00497 CommandHistory::updateActions()
00498 {
00499     m_actionCounts.clear();
00500 
00501     for (int undo = 0; undo <= 1; ++undo) {
00502 
00503         QAction *action(undo ? m_undoAction : m_redoAction);
00504         QAction *menuAction(undo ? m_undoMenuAction : m_redoMenuAction);
00505         QMenu *menu(undo ? m_undoMenu : m_redoMenu);
00506         CommandStack &stack(undo ? m_undoStack : m_redoStack);
00507 
00508         if (stack.empty()) {
00509 
00510             QString text(undo ? tr("Nothing to undo") : tr("Nothing to redo"));
00511 
00512             action->setEnabled(false);
00513             action->setText(text);
00514 
00515             menuAction->setEnabled(false);
00516             menuAction->setText(text);
00517 
00518         } else {
00519 
00520             action->setEnabled(true);
00521             menuAction->setEnabled(true);
00522 
00523             QString commandName = stack.top()->getName();
00524             commandName.replace(QRegExp("&"), "");
00525 
00526             QString text = (undo ? tr("&Undo %1") : tr("Re&do %1"))
00527                 .arg(commandName);
00528 
00529             action->setText(text);
00530             menuAction->setText(text);
00531         }
00532 
00533         menu->clear();
00534 
00535         CommandStack tempStack;
00536         int j = 0;
00537 
00538         while (j < m_menuLimit && !stack.empty()) {
00539 
00540             Command *command = stack.top();
00541             tempStack.push(command);
00542             stack.pop();
00543 
00544             QString commandName = command->getName();
00545             commandName.replace(QRegExp("&"), "");
00546 
00547             QString text;
00548             if (undo) text = tr("&Undo %1").arg(commandName);
00549             else      text = tr("Re&do %1").arg(commandName);
00550             
00551             QAction *action = menu->addAction(text);
00552             m_actionCounts[action] = j++;
00553         }
00554 
00555         while (!tempStack.empty()) {
00556             stack.push(tempStack.top());
00557             tempStack.pop();
00558         }
00559     }
00560 }
00561