svgui  1.9
CommandHistory.cpp
Go to the documentation of this file.
1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */
2 
3 /*
4  Sonic Visualiser
5  An audio file viewer and annotation editor.
6  Centre for Digital Music, Queen Mary, University of London.
7 
8  This program is free software; you can redistribute it and/or
9  modify it under the terms of the GNU General Public License as
10  published by the Free Software Foundation; either version 2 of the
11  License, or (at your option) any later version. See the file
12  COPYING included with this distribution for more information.
13 */
14 
15 /*
16  This is a modified version of a source file from the Rosegarden
17  MIDI and audio sequencer and notation editor, copyright 2000-2006
18  Chris Cannam, distributed under the GNU General Public License.
19 
20  This file contains traces of the KCommandHistory class from the KDE
21  project, copyright 2000 Werner Trobin and David Faure and
22  distributed under the GNU Lesser General Public License.
23 */
24 
25 #include "CommandHistory.h"
26 
27 #include "base/Command.h"
28 
29 #include <QRegExp>
30 #include <QMenu>
31 #include <QToolBar>
32 #include <QString>
33 #include <QTimer>
34 #include <QAction>
35 
36 #include <iostream>
37 
38 #include <typeinfo>
39 
40 //#define DEBUG_COMMAND_HISTORY 1
41 
43 
45  m_undoLimit(50),
46  m_redoLimit(50),
47  m_menuLimit(15),
48  m_savedAt(0),
49  m_currentCompound(0),
50  m_executeCompound(false),
51  m_currentBundle(0),
52  m_bundling(false),
53  m_bundleTimer(0),
54  m_bundleTimeout(3000)
55 {
56  m_undoAction = new QAction(QIcon(":/icons/undo.png"), tr("&Undo"), this);
57  m_undoAction->setShortcut(tr("Ctrl+Z"));
58  m_undoAction->setStatusTip(tr("Undo the last editing operation"));
59  connect(m_undoAction, SIGNAL(triggered()), this, SLOT(undo()));
60 
61  m_undoMenuAction = new QAction(QIcon(":/icons/undo.png"), tr("&Undo"), this);
62  connect(m_undoMenuAction, SIGNAL(triggered()), this, SLOT(undo()));
63 
64  m_undoMenu = new QMenu(tr("&Undo"));
65  m_undoMenuAction->setMenu(m_undoMenu);
66  connect(m_undoMenu, SIGNAL(triggered(QAction *)),
67  this, SLOT(undoActivated(QAction*)));
68 
69  m_redoAction = new QAction(QIcon(":/icons/redo.png"), tr("Re&do"), this);
70  m_redoAction->setShortcut(tr("Ctrl+Shift+Z"));
71  m_redoAction->setStatusTip(tr("Redo the last operation that was undone"));
72  connect(m_redoAction, SIGNAL(triggered()), this, SLOT(redo()));
73 
74  m_redoMenuAction = new QAction(QIcon(":/icons/redo.png"), tr("Re&do"), this);
75  connect(m_redoMenuAction, SIGNAL(triggered()), this, SLOT(redo()));
76 
77  m_redoMenu = new QMenu(tr("Re&do"));
78  m_redoMenuAction->setMenu(m_redoMenu);
79  connect(m_redoMenu, SIGNAL(triggered(QAction *)),
80  this, SLOT(redoActivated(QAction*)));
81 }
82 
84 {
85  m_savedAt = -1;
88 
89  delete m_undoMenu;
90  delete m_redoMenu;
91 }
92 
95 {
96  if (!m_instance) m_instance = new CommandHistory();
97  return m_instance;
98 }
99 
100 void
102 {
103 #ifdef DEBUG_COMMAND_HISTORY
104  cerr << "CommandHistory::clear()" << endl;
105 #endif
106  closeBundle();
107  m_savedAt = -1;
110  updateActions();
111 }
112 
113 void
115 {
116  menu->addAction(m_undoAction);
117  menu->addAction(m_redoAction);
118 }
119 
120 void
122 {
123  toolbar->addAction(m_undoMenuAction);
124  toolbar->addAction(m_redoMenuAction);
125 }
126 
127 void
128 CommandHistory::addCommand(Command *command)
129 {
130  if (!command) return;
131 
132  if (m_currentCompound) {
134  return;
135  }
136 
137  addCommand(command, true);
138 }
139 
140 void
141 CommandHistory::addCommand(Command *command, bool execute, bool bundle)
142 {
143  if (!command) return;
144 
145 #ifdef DEBUG_COMMAND_HISTORY
146  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;
147 #endif
148 
149  if (m_currentCompound) {
150  addToCompound(command, execute);
151  return;
152  }
153 
154  if (bundle) {
155  addToBundle(command, execute);
156  return;
157  } else if (m_currentBundle) {
158  closeBundle();
159  }
160 
161 #ifdef DEBUG_COMMAND_HISTORY
162  if (!m_redoStack.empty()) {
163  cerr << "CommandHistory::clearing redo stack" << endl;
164  }
165 #endif
166 
167  // We can't redo after adding a command
169 
170  // can we reach savedAt?
171  if ((int)m_undoStack.size() < m_savedAt) m_savedAt = -1; // nope
172 
173  m_undoStack.push(command);
174  clipCommands();
175 
176  if (execute) {
177  command->execute();
178  }
179 
180  // Emit even if we aren't executing the command, because
181  // someone must have executed it for this to make any sense
182  emit commandExecuted();
183  emit commandExecuted(command);
184  if (!m_bundling) emit activity(command->getName());
185 
186  updateActions();
187 }
188 
189 void
190 CommandHistory::addToBundle(Command *command, bool execute)
191 {
192  if (m_currentBundle) {
193  if (!command || (command->getName() != m_currentBundleName)) {
194 #ifdef DEBUG_COMMAND_HISTORY
195  cerr << "CommandHistory::addToBundle: " << command->getName()
196  << ": closing current bundle" << endl;
197 #endif
198  closeBundle();
199  }
200  }
201 
202  if (!command) return;
203 
204  if (!m_currentBundle) {
205 
206 #ifdef DEBUG_COMMAND_HISTORY
207  cerr << "CommandHistory::addToBundle: " << command->getName()
208  << ": creating new bundle" << endl;
209 #endif
210 
211  // need to addCommand before setting m_currentBundle, as addCommand
212  // with bundle false will reset m_currentBundle to 0
213  MacroCommand *mc = new BundleCommand(command->getName());
214  m_bundling = true;
215  addCommand(mc, false);
216  m_bundling = false;
217  m_currentBundle = mc;
218  m_currentBundleName = command->getName();
219  }
220 
221 #ifdef DEBUG_COMMAND_HISTORY
222  cerr << "CommandHistory::addToBundle: " << command->getName()
223  << ": adding to bundle" << endl;
224 #endif
225 
226  if (execute) command->execute();
227  m_currentBundle->addCommand(command);
228 
229  // Emit even if we aren't executing the command, because
230  // someone must have executed it for this to make any sense
231  emit commandExecuted();
232  emit commandExecuted(command);
233 
234  updateActions();
235 
236  delete m_bundleTimer;
237  m_bundleTimer = new QTimer(this);
238  connect(m_bundleTimer, SIGNAL(timeout()), this, SLOT(bundleTimerTimeout()));
240 }
241 
242 void
244 {
245  if (m_currentBundle) {
246 #ifdef DEBUG_COMMAND_HISTORY
247  cerr << "CommandHistory::closeBundle" << endl;
248 #endif
249  emit activity(m_currentBundle->getName());
250  }
251  m_currentBundle = 0;
252  m_currentBundleName = "";
253 }
254 
255 void
257 {
258 #ifdef DEBUG_COMMAND_HISTORY
259  cerr << "CommandHistory::bundleTimerTimeout: bundle is " << m_currentBundle << endl;
260 #endif
261 
262  closeBundle();
263 }
264 
265 void
266 CommandHistory::addToCompound(Command *command, bool execute)
267 {
268  if (!m_currentCompound) {
269  cerr << "CommandHistory::addToCompound: ERROR: no compound operation in progress!" << endl;
270  return;
271  }
272 
273 #ifdef DEBUG_COMMAND_HISTORY
274  cerr << "CommandHistory::addToCompound[" << m_currentCompound->getName() << "]: " << command->getName() << " (exec: " << execute << ")" << endl;
275 #endif
276 
277  if (execute) command->execute();
278  m_currentCompound->addCommand(command);
279 }
280 
281 void
282 CommandHistory::startCompoundOperation(QString name, bool execute)
283 {
284  if (m_currentCompound) {
285  cerr << "CommandHistory::startCompoundOperation: ERROR: compound operation already in progress!" << endl;
286  cerr << "(name is " << m_currentCompound->getName() << ")" << endl;
287  return;
288  }
289 
290 #ifdef DEBUG_COMMAND_HISTORY
291  cerr << "CommandHistory::startCompoundOperation: " << name << " (exec: " << execute << ")" << endl;
292 #endif
293 
294  closeBundle();
295 
296  m_currentCompound = new MacroCommand(name);
297  m_executeCompound = execute;
298 }
299 
300 void
302 {
303  if (!m_currentCompound) {
304  cerr << "CommandHistory::endCompoundOperation: ERROR: no compound operation in progress!" << endl;
305  return;
306  }
307 
308 #ifdef DEBUG_COMMAND_HISTORY
309  cerr << "CommandHistory::endCompoundOperation: " << m_currentCompound->getName() << endl;
310 #endif
311 
312  MacroCommand *toAdd = m_currentCompound;
313  m_currentCompound = 0;
314 
315  if (toAdd->haveCommands()) {
316 
317  // We don't execute the macro command here, because we have
318  // been executing the individual commands as we went along if
319  // m_executeCompound was true.
320  addCommand(toAdd, false);
321  }
322 }
323 
324 void
326 {
327  addCommand(command, false);
328 }
329 
330 void
332 {
333  addCommand(command, true);
334 }
335 
336 void
338 {
339  if (m_undoStack.empty()) return;
340 
341 #ifdef DEBUG_COMMAND_HISTORY
342  cerr << "CommandHistory::undo()" << endl;
343 #endif
344 
345  closeBundle();
346 
347  Command *command = m_undoStack.top();
348  command->unexecute();
349  emit commandExecuted();
350  emit commandUnexecuted(command);
351  emit activity(tr("Undo %1").arg(command->getName()));
352 
353  m_redoStack.push(command);
354  m_undoStack.pop();
355 
356  clipCommands();
357  updateActions();
358 
359  if ((int)m_undoStack.size() == m_savedAt) emit documentRestored();
360 }
361 
362 void
364 {
365  if (m_redoStack.empty()) return;
366 
367 #ifdef DEBUG_COMMAND_HISTORY
368  cerr << "CommandHistory::redo()" << endl;
369 #endif
370 
371  closeBundle();
372 
373  Command *command = m_redoStack.top();
374  command->execute();
375  emit commandExecuted();
376  emit commandExecuted(command);
377  emit activity(tr("Redo %1").arg(command->getName()));
378 
379  m_undoStack.push(command);
380  m_redoStack.pop();
381  // no need to clip
382 
383  updateActions();
384 
385  if ((int)m_undoStack.size() == m_savedAt) emit documentRestored();
386 }
387 
388 void
390 {
391  if (limit > 0 && limit != m_undoLimit) {
392  m_undoLimit = limit;
393  clipCommands();
394  }
395 }
396 
397 void
399 {
400  if (limit > 0 && limit != m_redoLimit) {
401  m_redoLimit = limit;
402  clipCommands();
403  }
404 }
405 
406 void
408 {
409  m_menuLimit = limit;
410  updateActions();
411 }
412 
413 void
415 {
416  m_bundleTimeout = ms;
417 }
418 
419 void
421 {
422  closeBundle();
423  m_savedAt = m_undoStack.size();
424 }
425 
426 void
428 {
429  if ((int)m_undoStack.size() > m_undoLimit) {
430  m_savedAt -= (m_undoStack.size() - m_undoLimit);
431  }
432 
435 }
436 
437 void
439 {
440  int i;
441 
442  if ((int)stack.size() > limit) {
443 
444  CommandStack tempStack;
445 
446  for (i = 0; i < limit; ++i) {
447 #ifdef DEBUG_COMMAND_HISTORY
448  Command *command = stack.top();
449  cerr << "CommandHistory::clipStack: Saving recent command: " << command->getName() << " at " << command << endl;
450 #endif
451  tempStack.push(stack.top());
452  stack.pop();
453  }
454 
455  clearStack(stack);
456 
457  for (i = 0; i < m_undoLimit; ++i) {
458  stack.push(tempStack.top());
459  tempStack.pop();
460  }
461  }
462 }
463 
464 void
466 {
467  while (!stack.empty()) {
468  Command *command = stack.top();
469  // Not safe to call getName() on a command about to be deleted
470 #ifdef DEBUG_COMMAND_HISTORY
471  cerr << "CommandHistory::clearStack: About to delete command " << command << endl;
472 #endif
473  delete command;
474  stack.pop();
475  }
476 }
477 
478 void
480 {
481  int pos = m_actionCounts[action];
482  for (int i = 0; i <= pos; ++i) {
483  undo();
484  }
485 }
486 
487 void
489 {
490  int pos = m_actionCounts[action];
491  for (int i = 0; i <= pos; ++i) {
492  redo();
493  }
494 }
495 
496 void
498 {
499  m_actionCounts.clear();
500 
501  for (int undo = 0; undo <= 1; ++undo) {
502 
503  QAction *action(undo ? m_undoAction : m_redoAction);
504  QAction *menuAction(undo ? m_undoMenuAction : m_redoMenuAction);
505  QMenu *menu(undo ? m_undoMenu : m_redoMenu);
507 
508  if (stack.empty()) {
509 
510  QString text(undo ? tr("Nothing to undo") : tr("Nothing to redo"));
511 
512  action->setEnabled(false);
513  action->setText(text);
514 
515  menuAction->setEnabled(false);
516  menuAction->setText(text);
517 
518  } else {
519 
520  action->setEnabled(true);
521  menuAction->setEnabled(true);
522 
523  QString commandName = stack.top()->getName();
524  commandName.replace(QRegExp("&"), "");
525 
526  QString text = (undo ? tr("&Undo %1") : tr("Re&do %1"))
527  .arg(commandName);
528 
529  action->setText(text);
530  menuAction->setText(text);
531  }
532 
533  menu->clear();
534 
535  CommandStack tempStack;
536  int j = 0;
537 
538  while (j < m_menuLimit && !stack.empty()) {
539 
540  Command *command = stack.top();
541  tempStack.push(command);
542  stack.pop();
543 
544  QString commandName = command->getName();
545  commandName.replace(QRegExp("&"), "");
546 
547  QString text;
548  if (undo) text = tr("&Undo %1").arg(commandName);
549  else text = tr("Re&do %1").arg(commandName);
550 
551  QAction *action = menu->addAction(text);
552  m_actionCounts[action] = j++;
553  }
554 
555  while (!tempStack.empty()) {
556  stack.push(tempStack.top());
557  tempStack.pop();
558  }
559  }
560 }
561 
void addToCompound(Command *command, bool execute)
void setBundleTimeout(int msec)
Set the time after which a bundle will be closed if nothing is added.
void addExecutedCommand(Command *)
Add a command to the history that has already been executed, without executing it again.
QAction * m_redoAction
void addCommandAndExecute(Command *)
Add a command to the history and also execute it.
CommandStack m_undoStack
void startCompoundOperation(QString name, bool execute)
Start recording commands to batch up into a single compound command.
void endCompoundOperation()
Finish recording commands and store the compound command.
std::stack< Command * > CommandStack
void addCommand(Command *command)
Add a command to the command history.
void setUndoLimit(int limit)
Set the maximum number of items in the undo history.
void activity(QString)
Emitted when some activity happened (for activity logging).
void setMenuLimit(int limit)
Set the maximum number of items in the menus.
QAction * m_redoMenuAction
QAction * m_undoMenuAction
void documentRestored()
Emitted when the undo/redo stack has reached the same state at which the documentSaved slot was last ...
void registerToolbar(QToolBar *toolbar)
void commandUnexecuted(Command *)
Emitted whenever a command has just been unexecuted, whether by addCommand or undo.
QTimer * m_bundleTimer
void redoActivated(QAction *)
CommandStack m_redoStack
std::map< QAction *, int > m_actionCounts
void undoActivated(QAction *)
The CommandHistory class stores a list of executed commands and maintains Undo and Redo actions synch...
static CommandHistory * getInstance()
QAction * m_undoAction
void registerMenu(QMenu *menu)
static CommandHistory * m_instance
void setRedoLimit(int limit)
Set the maximum number of items in the redo history.
void commandExecuted()
Emitted whenever a command has just been executed or unexecuted, whether by addCommand,...
virtual ~CommandHistory()
void clipStack(CommandStack &stack, int limit)
void addToBundle(Command *command, bool execute)
MacroCommand * m_currentBundle
virtual void documentSaved()
Checkpoint function that should be called when the document is saved.
MacroCommand * m_currentCompound
QString m_currentBundleName
void clearStack(CommandStack &stack)