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 #include "base/Profiler.h"
29 
30 #include "IconLoader.h"
31 
32 #include <QRegExp>
33 #include <QMenu>
34 #include <QToolBar>
35 #include <QString>
36 #include <QTimer>
37 #include <QAction>
38 
39 #include <iostream>
40 
41 #include <typeinfo>
42 
43 //#define DEBUG_COMMAND_HISTORY 1
44 
46 
48  m_undoLimit(50),
49  m_redoLimit(50),
50  m_menuLimit(15),
51  m_savedAt(0),
52  m_currentCompound(nullptr),
53  m_executeCompound(false),
54  m_currentBundle(nullptr),
55  m_bundling(false),
56  m_bundleTimer(nullptr),
57  m_bundleTimeout(3000)
58 {
59  IconLoader loader;
60  QIcon undoIcon(loader.load("undo"));
61  QIcon redoIcon(loader.load("redo"));
62 
63  m_undoAction = new QAction(undoIcon, ("&Undo"), this);
64  m_undoAction->setShortcut(tr("Ctrl+Z"));
65  m_undoAction->setStatusTip(tr("Undo the last editing operation"));
66  connect(m_undoAction, SIGNAL(triggered()), this, SLOT(undo()));
67 
68  m_undoMenuAction = new QAction(undoIcon, tr("&Undo"), this);
69  connect(m_undoMenuAction, SIGNAL(triggered()), this, SLOT(undo()));
70 
71  m_undoMenu = new QMenu(tr("&Undo"));
72  m_undoMenuAction->setMenu(m_undoMenu);
73  connect(m_undoMenu, SIGNAL(triggered(QAction *)),
74  this, SLOT(undoActivated(QAction*)));
75 
76  m_redoAction = new QAction(redoIcon, tr("Re&do"), this);
77  m_redoAction->setShortcut(tr("Ctrl+Shift+Z"));
78  m_redoAction->setStatusTip(tr("Redo the last operation that was undone"));
79  connect(m_redoAction, SIGNAL(triggered()), this, SLOT(redo()));
80 
81  m_redoMenuAction = new QAction(redoIcon, tr("Re&do"), this);
82  connect(m_redoMenuAction, SIGNAL(triggered()), this, SLOT(redo()));
83 
84  m_redoMenu = new QMenu(tr("Re&do"));
85  m_redoMenuAction->setMenu(m_redoMenu);
86  connect(m_redoMenu, SIGNAL(triggered(QAction *)),
87  this, SLOT(redoActivated(QAction*)));
88 }
89 
91 {
92  m_savedAt = -1;
95 
96  delete m_undoMenu;
97  delete m_redoMenu;
98 }
99 
102 {
103  if (!m_instance) m_instance = new CommandHistory();
104  return m_instance;
105 }
106 
107 void
109 {
110 #ifdef DEBUG_COMMAND_HISTORY
111  cerr << "CommandHistory::clear()" << endl;
112 #endif
113  closeBundle();
114  m_savedAt = -1;
117  updateActions();
118 }
119 
120 void
122 {
123  menu->addAction(m_undoAction);
124  menu->addAction(m_redoAction);
125 }
126 
127 void
129 {
130  toolbar->addAction(m_undoMenuAction);
131  toolbar->addAction(m_redoMenuAction);
132 }
133 
134 void
135 CommandHistory::addCommand(Command *command)
136 {
137  if (!command) return;
138 
139  if (m_currentCompound) {
141  return;
142  }
143 
144  addCommand(command, true);
145 }
146 
147 void
148 CommandHistory::addCommand(Command *command, bool execute, bool bundle)
149 {
150  if (!command) return;
151 
152 #ifdef DEBUG_COMMAND_HISTORY
153  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;
154 #endif
155 
156  if (m_currentCompound) {
157  addToCompound(command, execute);
158  return;
159  }
160 
161  if (bundle) {
162  addToBundle(command, execute);
163  return;
164  } else if (m_currentBundle) {
165  closeBundle();
166  }
167 
168 #ifdef DEBUG_COMMAND_HISTORY
169  if (!m_redoStack.empty()) {
170  cerr << "CommandHistory::clearing redo stack" << endl;
171  }
172 #endif
173 
174  // We can't redo after adding a command
176 
177  // can we reach savedAt?
178  if ((int)m_undoStack.size() < m_savedAt) m_savedAt = -1; // nope
179 
180  m_undoStack.push(command);
181  clipCommands();
182 
183  if (execute) {
184  command->execute();
185  }
186 
187  // Emit even if we aren't executing the command, because
188  // someone must have executed it for this to make any sense
189  emit commandExecuted();
190  emit commandExecuted(command);
191  if (!m_bundling) emit activity(command->getName());
192 
193  updateActions();
194 }
195 
196 void
197 CommandHistory::addToBundle(Command *command, bool execute)
198 {
199  if (m_currentBundle) {
200  if (!command || (command->getName() != m_currentBundleName)) {
201 #ifdef DEBUG_COMMAND_HISTORY
202  cerr << "CommandHistory::addToBundle: " << command->getName()
203  << ": closing current bundle" << endl;
204 #endif
205  closeBundle();
206  }
207  }
208 
209  if (!command) return;
210 
211  if (!m_currentBundle) {
212 
213 #ifdef DEBUG_COMMAND_HISTORY
214  cerr << "CommandHistory::addToBundle: " << command->getName()
215  << ": creating new bundle" << endl;
216 #endif
217 
218  // need to addCommand before setting m_currentBundle, as addCommand
219  // with bundle false will reset m_currentBundle to 0
220  MacroCommand *mc = new BundleCommand(command->getName());
221  m_bundling = true;
222  addCommand(mc, false);
223  m_bundling = false;
224  m_currentBundle = mc;
225  m_currentBundleName = command->getName();
226  }
227 
228 #ifdef DEBUG_COMMAND_HISTORY
229  cerr << "CommandHistory::addToBundle: " << command->getName()
230  << ": adding to bundle" << endl;
231 #endif
232 
233  if (execute) command->execute();
234  m_currentBundle->addCommand(command);
235 
236  // Emit even if we aren't executing the command, because
237  // someone must have executed it for this to make any sense
238  emit commandExecuted();
239  emit commandExecuted(command);
240 
241  updateActions();
242 
243  delete m_bundleTimer;
244  m_bundleTimer = new QTimer(this);
245  connect(m_bundleTimer, SIGNAL(timeout()), this, SLOT(bundleTimerTimeout()));
247 }
248 
249 void
251 {
252  if (m_currentBundle) {
253 #ifdef DEBUG_COMMAND_HISTORY
254  cerr << "CommandHistory::closeBundle" << endl;
255 #endif
256  emit activity(m_currentBundle->getName());
257  }
258  m_currentBundle = nullptr;
259  m_currentBundleName = "";
260 }
261 
262 void
264 {
265 #ifdef DEBUG_COMMAND_HISTORY
266  cerr << "CommandHistory::bundleTimerTimeout: bundle is " << m_currentBundle << endl;
267 #endif
268 
269  closeBundle();
270 }
271 
272 void
273 CommandHistory::addToCompound(Command *command, bool execute)
274 {
275  if (!m_currentCompound) {
276  cerr << "CommandHistory::addToCompound: ERROR: no compound operation in progress!" << endl;
277  return;
278  }
279 
280 #ifdef DEBUG_COMMAND_HISTORY
281  cerr << "CommandHistory::addToCompound[" << m_currentCompound->getName() << "]: " << command->getName() << " (exec: " << execute << ")" << endl;
282 #endif
283 
284  if (execute) command->execute();
285  m_currentCompound->addCommand(command);
286 }
287 
288 void
289 CommandHistory::startCompoundOperation(QString name, bool execute)
290 {
291  if (m_currentCompound) {
292  cerr << "CommandHistory::startCompoundOperation: ERROR: compound operation already in progress!" << endl;
293  cerr << "(name is " << m_currentCompound->getName() << ")" << endl;
294  return;
295  }
296 
297 #ifdef DEBUG_COMMAND_HISTORY
298  cerr << "CommandHistory::startCompoundOperation: " << name << " (exec: " << execute << ")" << endl;
299 #endif
300 
301  closeBundle();
302 
303  m_currentCompound = new MacroCommand(name);
304  m_executeCompound = execute;
305 }
306 
307 void
309 {
310  if (!m_currentCompound) {
311  cerr << "CommandHistory::endCompoundOperation: ERROR: no compound operation in progress!" << endl;
312  return;
313  }
314 
315 #ifdef DEBUG_COMMAND_HISTORY
316  cerr << "CommandHistory::endCompoundOperation: " << m_currentCompound->getName() << endl;
317 #endif
318 
319  MacroCommand *toAdd = m_currentCompound;
320  m_currentCompound = nullptr;
321 
322  if (toAdd->haveCommands()) {
323 
324  // We don't execute the macro command here, because we have
325  // been executing the individual commands as we went along if
326  // m_executeCompound was true.
327  addCommand(toAdd, false);
328  }
329 }
330 
331 void
333 {
334  addCommand(command, false);
335 }
336 
337 void
339 {
340  addCommand(command, true);
341 }
342 
343 void
345 {
346  if (m_undoStack.empty()) return;
347 
348 #ifdef DEBUG_COMMAND_HISTORY
349  cerr << "CommandHistory::undo()" << endl;
350 #endif
351 
352  closeBundle();
353 
354  Command *command = m_undoStack.top();
355  command->unexecute();
356  emit commandExecuted();
357  emit commandUnexecuted(command);
358  emit activity(tr("Undo %1").arg(command->getName()));
359 
360  m_redoStack.push(command);
361  m_undoStack.pop();
362 
363  clipCommands();
364  updateActions();
365 
366  if ((int)m_undoStack.size() == m_savedAt) emit documentRestored();
367 }
368 
369 void
371 {
372  if (m_redoStack.empty()) return;
373 
374 #ifdef DEBUG_COMMAND_HISTORY
375  cerr << "CommandHistory::redo()" << endl;
376 #endif
377 
378  closeBundle();
379 
380  Command *command = m_redoStack.top();
381  command->execute();
382  emit commandExecuted();
383  emit commandExecuted(command);
384  emit activity(tr("Redo %1").arg(command->getName()));
385 
386  m_undoStack.push(command);
387  m_redoStack.pop();
388  // no need to clip
389 
390  updateActions();
391 
392  if ((int)m_undoStack.size() == m_savedAt) emit documentRestored();
393 }
394 
395 void
397 {
398  if (limit > 0 && limit != m_undoLimit) {
399  m_undoLimit = limit;
400  clipCommands();
401  }
402 }
403 
404 void
406 {
407  if (limit > 0 && limit != m_redoLimit) {
408  m_redoLimit = limit;
409  clipCommands();
410  }
411 }
412 
413 void
415 {
416  m_menuLimit = limit;
417  updateActions();
418 }
419 
420 void
422 {
423  m_bundleTimeout = ms;
424 }
425 
426 void
428 {
429  closeBundle();
430  m_savedAt = int(m_undoStack.size());
431 }
432 
433 void
435 {
436  if (int(m_undoStack.size()) > m_undoLimit) {
437  m_savedAt -= (int(m_undoStack.size()) - m_undoLimit);
438  }
439 
442 }
443 
444 void
446 {
447  int i;
448 
449  if ((int)stack.size() > limit) {
450 
451  CommandStack tempStack;
452 
453  for (i = 0; i < limit; ++i) {
454 #ifdef DEBUG_COMMAND_HISTORY
455  Command *command = stack.top();
456  cerr << "CommandHistory::clipStack: Saving recent command: " << command->getName() << " at " << command << endl;
457 #endif
458  tempStack.push(stack.top());
459  stack.pop();
460  }
461 
462  clearStack(stack);
463 
464  for (i = 0; i < m_undoLimit; ++i) {
465  stack.push(tempStack.top());
466  tempStack.pop();
467  }
468  }
469 }
470 
471 void
473 {
474  Profiler profiler("CommandHistory::clearStack");
475 
476  while (!stack.empty()) {
477  Command *command = stack.top();
478  // Not safe to call getName() on a command about to be deleted
479 #ifdef DEBUG_COMMAND_HISTORY
480  cerr << "CommandHistory::clearStack: About to delete command " << command << endl;
481 #endif
482  delete command;
483  stack.pop();
484  }
485 }
486 
487 void
489 {
490  int pos = m_actionCounts[action];
491  for (int i = 0; i <= pos; ++i) {
492  undo();
493  }
494 }
495 
496 void
498 {
499  int pos = m_actionCounts[action];
500  for (int i = 0; i <= pos; ++i) {
501  redo();
502  }
503 }
504 
505 void
507 {
508  m_actionCounts.clear();
509 
510  for (int undo = 0; undo <= 1; ++undo) {
511 
512  QAction *action(undo ? m_undoAction : m_redoAction);
513  QAction *menuAction(undo ? m_undoMenuAction : m_redoMenuAction);
514  QMenu *menu(undo ? m_undoMenu : m_redoMenu);
516 
517  if (stack.empty()) {
518 
519  QString text(undo ? tr("Nothing to undo") : tr("Nothing to redo"));
520 
521  action->setEnabled(false);
522  action->setText(text);
523 
524  menuAction->setEnabled(false);
525  menuAction->setText(text);
526 
527  } else {
528 
529  action->setEnabled(true);
530  menuAction->setEnabled(true);
531 
532  QString commandName = stack.top()->getName();
533  commandName.replace(QRegExp("&"), "");
534 
535  QString text = (undo ? tr("&Undo %1") : tr("Re&do %1"))
536  .arg(commandName);
537 
538  action->setText(text);
539  menuAction->setText(text);
540  }
541 
542  menu->clear();
543 
544  CommandStack tempStack;
545  int j = 0;
546 
547  while (j < m_menuLimit && !stack.empty()) {
548 
549  Command *command = stack.top();
550  tempStack.push(command);
551  stack.pop();
552 
553  QString commandName = command->getName();
554  commandName.replace(QRegExp("&"), "");
555 
556  QString text;
557  if (undo) text = tr("&Undo %1").arg(commandName);
558  else text = tr("Re&do %1").arg(commandName);
559 
560  QAction *action = menu->addAction(text);
561  m_actionCounts[action] = j++;
562  }
563 
564  while (!tempStack.empty()) {
565  stack.push(tempStack.top());
566  tempStack.pop();
567  }
568  }
569 }
570 
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, undo, or redo.
virtual ~CommandHistory()
void clipStack(CommandStack &stack, int limit)
void addToBundle(Command *command, bool execute)
MacroCommand * m_currentBundle
QIcon load(QString name)
Definition: IconLoader.cpp:66
virtual void documentSaved()
Checkpoint function that should be called when the document is saved.
MacroCommand * m_currentCompound
QString m_currentBundleName
void clearStack(CommandStack &stack)