Chris@49
|
1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */
|
Chris@16
|
2
|
Chris@16
|
3 /*
|
Chris@16
|
4 A waveform viewer and audio annotation editor.
|
Chris@16
|
5 Chris Cannam, Queen Mary University of London, 2005-2006
|
Chris@16
|
6
|
Chris@16
|
7 This is experimental software. Not for distribution.
|
Chris@16
|
8 */
|
Chris@16
|
9
|
Chris@16
|
10 /*
|
Chris@16
|
11 This is a modified version of a source file from the Rosegarden
|
Chris@16
|
12 MIDI and audio sequencer and notation editor, copyright 2000-2006
|
Chris@16
|
13 Chris Cannam, distributed under the GNU General Public License.
|
Chris@16
|
14
|
Chris@16
|
15 This file contains traces of the KCommandHistory class from the KDE
|
Chris@16
|
16 project, copyright 2000 Werner Trobin and David Faure and
|
Chris@16
|
17 distributed under the GNU Lesser General Public License.
|
Chris@16
|
18 */
|
Chris@16
|
19
|
Chris@17
|
20 #include "CommandHistory.h"
|
Chris@16
|
21
|
Chris@16
|
22 #include "Command.h"
|
Chris@16
|
23
|
Chris@16
|
24 #include <QRegExp>
|
Chris@16
|
25 #include <QMenu>
|
Chris@16
|
26 #include <QToolBar>
|
Chris@16
|
27 #include <QString>
|
Chris@47
|
28 #include <QTimer>
|
Chris@16
|
29
|
Chris@16
|
30 #include <iostream>
|
Chris@16
|
31
|
Chris@17
|
32 CommandHistory *CommandHistory::m_instance = 0;
|
Chris@17
|
33
|
Chris@17
|
34 CommandHistory::CommandHistory() :
|
Chris@16
|
35 m_undoLimit(50),
|
Chris@16
|
36 m_redoLimit(50),
|
Chris@46
|
37 m_menuLimit(15),
|
Chris@44
|
38 m_savedAt(0),
|
Chris@47
|
39 m_currentCompound(0),
|
Chris@47
|
40 m_executeCompound(false),
|
Chris@47
|
41 m_currentBundle(0),
|
Chris@47
|
42 m_bundleTimer(0),
|
Chris@47
|
43 m_bundleTimeout(5000)
|
Chris@16
|
44 {
|
Chris@16
|
45 m_undoAction = new QAction(QIcon(":/icons/undo.png"), tr("&Undo"), this);
|
Chris@16
|
46 m_undoAction->setShortcut(tr("Ctrl+Z"));
|
Chris@16
|
47 connect(m_undoAction, SIGNAL(triggered()), this, SLOT(undo()));
|
Chris@16
|
48
|
Chris@17
|
49 m_undoMenuAction = new QAction(QIcon(":/icons/undo.png"), tr("&Undo"), this);
|
Chris@17
|
50 connect(m_undoMenuAction, SIGNAL(triggered()), this, SLOT(undo()));
|
Chris@17
|
51
|
Chris@16
|
52 m_undoMenu = new QMenu(tr("&Undo"));
|
Chris@17
|
53 m_undoMenuAction->setMenu(m_undoMenu);
|
Chris@16
|
54 connect(m_undoMenu, SIGNAL(triggered(QAction *)),
|
Chris@16
|
55 this, SLOT(undoActivated(QAction*)));
|
Chris@16
|
56
|
Chris@17
|
57 m_redoAction = new QAction(QIcon(":/icons/redo.png"), tr("Re&do"), this);
|
Chris@16
|
58 m_redoAction->setShortcut(tr("Ctrl+Shift+Z"));
|
Chris@16
|
59 connect(m_redoAction, SIGNAL(triggered()), this, SLOT(redo()));
|
Chris@17
|
60
|
Chris@17
|
61 m_redoMenuAction = new QAction(QIcon(":/icons/redo.png"), tr("Re&do"), this);
|
Chris@17
|
62 connect(m_redoMenuAction, SIGNAL(triggered()), this, SLOT(redo()));
|
Chris@16
|
63
|
Chris@16
|
64 m_redoMenu = new QMenu(tr("Re&do"));
|
Chris@17
|
65 m_redoMenuAction->setMenu(m_redoMenu);
|
Chris@16
|
66 connect(m_redoMenu, SIGNAL(triggered(QAction *)),
|
Chris@16
|
67 this, SLOT(redoActivated(QAction*)));
|
Chris@16
|
68 }
|
Chris@16
|
69
|
Chris@17
|
70 CommandHistory::~CommandHistory()
|
Chris@16
|
71 {
|
Chris@16
|
72 m_savedAt = -1;
|
Chris@16
|
73 clearStack(m_undoStack);
|
Chris@16
|
74 clearStack(m_redoStack);
|
Chris@16
|
75
|
Chris@16
|
76 delete m_undoMenu;
|
Chris@16
|
77 delete m_redoMenu;
|
Chris@16
|
78 }
|
Chris@16
|
79
|
Chris@17
|
80 CommandHistory *
|
Chris@17
|
81 CommandHistory::getInstance()
|
Chris@17
|
82 {
|
Chris@17
|
83 if (!m_instance) m_instance = new CommandHistory();
|
Chris@17
|
84 return m_instance;
|
Chris@17
|
85 }
|
Chris@17
|
86
|
Chris@16
|
87 void
|
Chris@17
|
88 CommandHistory::clear()
|
Chris@16
|
89 {
|
Chris@47
|
90 std::cerr << "CommandHistory::clear()" << std::endl;
|
Chris@47
|
91 closeBundle();
|
Chris@16
|
92 m_savedAt = -1;
|
Chris@16
|
93 clearStack(m_undoStack);
|
Chris@16
|
94 clearStack(m_redoStack);
|
Chris@16
|
95 updateActions();
|
Chris@16
|
96 }
|
Chris@16
|
97
|
Chris@16
|
98 void
|
Chris@17
|
99 CommandHistory::registerMenu(QMenu *menu)
|
Chris@16
|
100 {
|
Chris@16
|
101 menu->addAction(m_undoAction);
|
Chris@16
|
102 menu->addAction(m_redoAction);
|
Chris@16
|
103 }
|
Chris@16
|
104
|
Chris@16
|
105 void
|
Chris@17
|
106 CommandHistory::registerToolbar(QToolBar *toolbar)
|
Chris@16
|
107 {
|
Chris@17
|
108 toolbar->addAction(m_undoMenuAction);
|
Chris@17
|
109 toolbar->addAction(m_redoMenuAction);
|
Chris@16
|
110 }
|
Chris@16
|
111
|
Chris@16
|
112 void
|
Chris@47
|
113 CommandHistory::addCommand(Command *command, bool execute, bool bundle)
|
Chris@16
|
114 {
|
Chris@16
|
115 if (!command) return;
|
Chris@16
|
116
|
Chris@47
|
117 if (m_currentCompound) {
|
Chris@47
|
118 addToCompound(command);
|
Chris@44
|
119 return;
|
Chris@44
|
120 }
|
Chris@44
|
121
|
Chris@47
|
122 if (bundle) {
|
Chris@47
|
123 addToBundle(command, execute);
|
Chris@47
|
124 return;
|
Chris@47
|
125 } else if (m_currentBundle) {
|
Chris@47
|
126 closeBundle();
|
Chris@47
|
127 }
|
Chris@47
|
128
|
Chris@47
|
129 std::cerr << "CommandHistory::addCommand: " << command->getName().toLocal8Bit().data() << " at " << command << std::endl;
|
Chris@16
|
130
|
Chris@16
|
131 // We can't redo after adding a command
|
Chris@47
|
132 std::cerr << "CommandHistory::clearing redo stack" << std::endl;
|
Chris@16
|
133 clearStack(m_redoStack);
|
Chris@16
|
134
|
Chris@16
|
135 // can we reach savedAt?
|
Chris@16
|
136 if ((int)m_undoStack.size() < m_savedAt) m_savedAt = -1; // nope
|
Chris@16
|
137
|
Chris@16
|
138 m_undoStack.push(command);
|
Chris@16
|
139 clipCommands();
|
Chris@16
|
140
|
Chris@16
|
141 if (execute) {
|
Chris@16
|
142 command->execute();
|
Chris@16
|
143 }
|
Chris@16
|
144
|
Chris@17
|
145 // Emit even if we aren't executing the command, because
|
Chris@17
|
146 // someone must have executed it for this to make any sense
|
Chris@17
|
147 emit commandExecuted();
|
Chris@17
|
148 emit commandExecuted(command);
|
Chris@17
|
149
|
Chris@16
|
150 updateActions();
|
Chris@16
|
151 }
|
Chris@16
|
152
|
Chris@16
|
153 void
|
Chris@47
|
154 CommandHistory::addToBundle(Command *command, bool execute)
|
Chris@44
|
155 {
|
Chris@47
|
156 if (m_currentBundle) {
|
Chris@47
|
157 if (!command || (command->getName() != m_currentBundleName)) {
|
Chris@47
|
158 closeBundle();
|
Chris@47
|
159 }
|
Chris@47
|
160 }
|
Chris@44
|
161
|
Chris@47
|
162 if (!command) return;
|
Chris@47
|
163
|
Chris@47
|
164 if (!m_currentBundle) {
|
Chris@47
|
165 // need to addCommand before setting m_currentBundle, as addCommand
|
Chris@47
|
166 // with bundle false will reset m_currentBundle to 0
|
Chris@47
|
167 MacroCommand *mc = new MacroCommand(command->getName());
|
Chris@47
|
168 addCommand(mc, false);
|
Chris@47
|
169 m_currentBundle = mc;
|
Chris@47
|
170 m_currentBundleName = command->getName();
|
Chris@47
|
171 }
|
Chris@47
|
172
|
Chris@47
|
173 if (execute) command->execute();
|
Chris@47
|
174 m_currentBundle->addCommand(command);
|
Chris@47
|
175
|
Chris@47
|
176 delete m_bundleTimer;
|
Chris@47
|
177 m_bundleTimer = new QTimer(this);
|
Chris@47
|
178 connect(m_bundleTimer, SIGNAL(timeout()), this, SLOT(bundleTimerTimeout()));
|
Chris@47
|
179 m_bundleTimer->start(m_bundleTimeout);
|
Chris@47
|
180 }
|
Chris@47
|
181
|
Chris@47
|
182 void
|
Chris@47
|
183 CommandHistory::closeBundle()
|
Chris@47
|
184 {
|
Chris@47
|
185 m_currentBundle = 0;
|
Chris@47
|
186 m_currentBundleName = "";
|
Chris@47
|
187 }
|
Chris@47
|
188
|
Chris@47
|
189 void
|
Chris@47
|
190 CommandHistory::bundleTimerTimeout()
|
Chris@47
|
191 {
|
Chris@47
|
192 closeBundle();
|
Chris@47
|
193 }
|
Chris@47
|
194
|
Chris@47
|
195 void
|
Chris@47
|
196 CommandHistory::addToCompound(Command *command)
|
Chris@47
|
197 {
|
Chris@47
|
198 std::cerr << "CommandHistory::addToCompound: " << command->getName().toLocal8Bit().data() << std::endl;
|
Chris@47
|
199
|
Chris@47
|
200 if (m_executeCompound) command->execute();
|
Chris@47
|
201 m_currentCompound->addCommand(command);
|
Chris@44
|
202 }
|
Chris@44
|
203
|
Chris@44
|
204 void
|
Chris@44
|
205 CommandHistory::startCompoundOperation(QString name, bool execute)
|
Chris@44
|
206 {
|
Chris@47
|
207 if (m_currentCompound) {
|
Chris@47
|
208 std::cerr << "CommandHistory::startCompoundOperation: ERROR: compound operation already in progress!" << std::endl;
|
Chris@47
|
209 std::cerr << "(name is " << m_currentCompound->getName().toLocal8Bit().data() << ")" << std::endl;
|
Chris@44
|
210 }
|
Chris@47
|
211
|
Chris@47
|
212 closeBundle();
|
Chris@47
|
213
|
Chris@47
|
214 m_currentCompound = new MacroCommand(name);
|
Chris@47
|
215 m_executeCompound = execute;
|
Chris@44
|
216 }
|
Chris@44
|
217
|
Chris@44
|
218 void
|
Chris@44
|
219 CommandHistory::endCompoundOperation()
|
Chris@44
|
220 {
|
Chris@47
|
221 if (!m_currentCompound) {
|
Chris@47
|
222 std::cerr << "CommandHistory::endCompoundOperation: ERROR: no compound operation in progress!" << std::endl;
|
Chris@44
|
223 }
|
Chris@44
|
224
|
Chris@47
|
225 Command *toAdd = m_currentCompound;
|
Chris@47
|
226 m_currentCompound = 0;
|
Chris@44
|
227
|
Chris@44
|
228 // We don't execute the macro command here, because we have been
|
Chris@44
|
229 // executing the individual commands as we went along if
|
Chris@47
|
230 // m_executeCompound was true.
|
Chris@44
|
231 addCommand(toAdd, false);
|
Chris@44
|
232 }
|
Chris@44
|
233
|
Chris@44
|
234 void
|
Chris@17
|
235 CommandHistory::addExecutedCommand(Command *command)
|
Chris@17
|
236 {
|
Chris@17
|
237 addCommand(command, false);
|
Chris@17
|
238 }
|
Chris@17
|
239
|
Chris@17
|
240 void
|
Chris@17
|
241 CommandHistory::addCommandAndExecute(Command *command)
|
Chris@17
|
242 {
|
Chris@17
|
243 addCommand(command, true);
|
Chris@17
|
244 }
|
Chris@17
|
245
|
Chris@17
|
246 void
|
Chris@17
|
247 CommandHistory::undo()
|
Chris@16
|
248 {
|
Chris@16
|
249 if (m_undoStack.empty()) return;
|
Chris@16
|
250
|
Chris@47
|
251 closeBundle();
|
Chris@47
|
252
|
Chris@16
|
253 Command *command = m_undoStack.top();
|
Chris@16
|
254 command->unexecute();
|
Chris@16
|
255 emit commandExecuted();
|
Chris@17
|
256 emit commandUnexecuted(command);
|
Chris@16
|
257
|
Chris@16
|
258 m_redoStack.push(command);
|
Chris@16
|
259 m_undoStack.pop();
|
Chris@16
|
260
|
Chris@16
|
261 clipCommands();
|
Chris@16
|
262 updateActions();
|
Chris@16
|
263
|
Chris@16
|
264 if ((int)m_undoStack.size() == m_savedAt) emit documentRestored();
|
Chris@16
|
265 }
|
Chris@16
|
266
|
Chris@16
|
267 void
|
Chris@17
|
268 CommandHistory::redo()
|
Chris@16
|
269 {
|
Chris@16
|
270 if (m_redoStack.empty()) return;
|
Chris@16
|
271
|
Chris@47
|
272 closeBundle();
|
Chris@47
|
273
|
Chris@16
|
274 Command *command = m_redoStack.top();
|
Chris@16
|
275 command->execute();
|
Chris@16
|
276 emit commandExecuted();
|
Chris@16
|
277 emit commandExecuted(command);
|
Chris@16
|
278
|
Chris@16
|
279 m_undoStack.push(command);
|
Chris@16
|
280 m_redoStack.pop();
|
Chris@16
|
281 // no need to clip
|
Chris@16
|
282
|
Chris@16
|
283 updateActions();
|
Chris@41
|
284
|
Chris@41
|
285 if ((int)m_undoStack.size() == m_savedAt) emit documentRestored();
|
Chris@16
|
286 }
|
Chris@16
|
287
|
Chris@16
|
288 void
|
Chris@17
|
289 CommandHistory::setUndoLimit(int limit)
|
Chris@16
|
290 {
|
Chris@16
|
291 if (limit > 0 && limit != m_undoLimit) {
|
Chris@16
|
292 m_undoLimit = limit;
|
Chris@16
|
293 clipCommands();
|
Chris@16
|
294 }
|
Chris@16
|
295 }
|
Chris@16
|
296
|
Chris@16
|
297 void
|
Chris@17
|
298 CommandHistory::setRedoLimit(int limit)
|
Chris@16
|
299 {
|
Chris@16
|
300 if (limit > 0 && limit != m_redoLimit) {
|
Chris@16
|
301 m_redoLimit = limit;
|
Chris@16
|
302 clipCommands();
|
Chris@16
|
303 }
|
Chris@16
|
304 }
|
Chris@16
|
305
|
Chris@16
|
306 void
|
Chris@46
|
307 CommandHistory::setMenuLimit(int limit)
|
Chris@46
|
308 {
|
Chris@46
|
309 m_menuLimit = limit;
|
Chris@46
|
310 updateActions();
|
Chris@46
|
311 }
|
Chris@46
|
312
|
Chris@46
|
313 void
|
Chris@47
|
314 CommandHistory::setBundleTimeout(int ms)
|
Chris@47
|
315 {
|
Chris@47
|
316 m_bundleTimeout = ms;
|
Chris@47
|
317 }
|
Chris@47
|
318
|
Chris@47
|
319 void
|
Chris@17
|
320 CommandHistory::documentSaved()
|
Chris@16
|
321 {
|
Chris@47
|
322 closeBundle();
|
Chris@16
|
323 m_savedAt = m_undoStack.size();
|
Chris@16
|
324 }
|
Chris@16
|
325
|
Chris@16
|
326 void
|
Chris@17
|
327 CommandHistory::clipCommands()
|
Chris@16
|
328 {
|
Chris@16
|
329 if ((int)m_undoStack.size() > m_undoLimit) {
|
Chris@16
|
330 m_savedAt -= (m_undoStack.size() - m_undoLimit);
|
Chris@16
|
331 }
|
Chris@16
|
332
|
Chris@16
|
333 clipStack(m_undoStack, m_undoLimit);
|
Chris@16
|
334 clipStack(m_redoStack, m_redoLimit);
|
Chris@16
|
335 }
|
Chris@16
|
336
|
Chris@16
|
337 void
|
Chris@17
|
338 CommandHistory::clipStack(CommandStack &stack, int limit)
|
Chris@16
|
339 {
|
Chris@16
|
340 int i;
|
Chris@16
|
341
|
Chris@16
|
342 if ((int)stack.size() > limit) {
|
Chris@16
|
343
|
Chris@16
|
344 CommandStack tempStack;
|
Chris@16
|
345
|
Chris@16
|
346 for (i = 0; i < limit; ++i) {
|
Chris@16
|
347 Command *command = stack.top();
|
Chris@47
|
348 std::cerr << "CommandHistory::clipStack: Saving recent command: " << command->getName().toLocal8Bit().data() << " at " << command << std::endl;
|
Chris@16
|
349 tempStack.push(stack.top());
|
Chris@16
|
350 stack.pop();
|
Chris@16
|
351 }
|
Chris@16
|
352
|
Chris@16
|
353 clearStack(stack);
|
Chris@16
|
354
|
Chris@16
|
355 for (i = 0; i < m_undoLimit; ++i) {
|
Chris@16
|
356 stack.push(tempStack.top());
|
Chris@16
|
357 tempStack.pop();
|
Chris@16
|
358 }
|
Chris@16
|
359 }
|
Chris@16
|
360 }
|
Chris@16
|
361
|
Chris@16
|
362 void
|
Chris@17
|
363 CommandHistory::clearStack(CommandStack &stack)
|
Chris@16
|
364 {
|
Chris@16
|
365 while (!stack.empty()) {
|
Chris@16
|
366 Command *command = stack.top();
|
Chris@46
|
367 // Not safe to call getName() on a command about to be deleted
|
Chris@47
|
368 std::cerr << "CommandHistory::clearStack: About to delete command " << command << std::endl;
|
Chris@16
|
369 delete command;
|
Chris@16
|
370 stack.pop();
|
Chris@16
|
371 }
|
Chris@16
|
372 }
|
Chris@16
|
373
|
Chris@16
|
374 void
|
Chris@17
|
375 CommandHistory::undoActivated(QAction *action)
|
Chris@16
|
376 {
|
Chris@16
|
377 int pos = m_actionCounts[action];
|
Chris@16
|
378 for (int i = 0; i <= pos; ++i) {
|
Chris@16
|
379 undo();
|
Chris@16
|
380 }
|
Chris@16
|
381 }
|
Chris@16
|
382
|
Chris@16
|
383 void
|
Chris@17
|
384 CommandHistory::redoActivated(QAction *action)
|
Chris@16
|
385 {
|
Chris@16
|
386 int pos = m_actionCounts[action];
|
Chris@16
|
387 for (int i = 0; i <= pos; ++i) {
|
Chris@16
|
388 redo();
|
Chris@16
|
389 }
|
Chris@16
|
390 }
|
Chris@16
|
391
|
Chris@16
|
392 void
|
Chris@17
|
393 CommandHistory::updateActions()
|
Chris@16
|
394 {
|
Chris@16
|
395 m_actionCounts.clear();
|
Chris@16
|
396
|
Chris@16
|
397 for (int undo = 0; undo <= 1; ++undo) {
|
Chris@16
|
398
|
Chris@17
|
399 QAction *action(undo ? m_undoAction : m_redoAction);
|
Chris@17
|
400 QAction *menuAction(undo ? m_undoMenuAction : m_redoMenuAction);
|
Chris@16
|
401 QMenu *menu(undo ? m_undoMenu : m_redoMenu);
|
Chris@16
|
402 CommandStack &stack(undo ? m_undoStack : m_redoStack);
|
Chris@16
|
403
|
Chris@17
|
404 if (stack.empty()) {
|
Chris@17
|
405
|
Chris@17
|
406 QString text(undo ? tr("Nothing to undo") : tr("Nothing to redo"));
|
Chris@17
|
407
|
Chris@17
|
408 action->setEnabled(false);
|
Chris@17
|
409 action->setText(text);
|
Chris@17
|
410
|
Chris@17
|
411 menuAction->setEnabled(false);
|
Chris@17
|
412 menuAction->setText(text);
|
Chris@17
|
413
|
Chris@17
|
414 } else {
|
Chris@17
|
415
|
Chris@17
|
416 action->setEnabled(true);
|
Chris@17
|
417 menuAction->setEnabled(true);
|
Chris@17
|
418
|
Chris@17
|
419 QString commandName = stack.top()->getName();
|
Chris@17
|
420 commandName.replace(QRegExp("&"), "");
|
Chris@17
|
421
|
Chris@17
|
422 QString text = (undo ? tr("&Undo %1") : tr("Re&do %1"))
|
Chris@17
|
423 .arg(commandName);
|
Chris@17
|
424
|
Chris@17
|
425 action->setText(text);
|
Chris@17
|
426 menuAction->setText(text);
|
Chris@17
|
427 }
|
Chris@17
|
428
|
Chris@16
|
429 menu->clear();
|
Chris@16
|
430
|
Chris@16
|
431 CommandStack tempStack;
|
Chris@16
|
432 int j = 0;
|
Chris@16
|
433
|
Chris@46
|
434 while (j < m_menuLimit && !stack.empty()) {
|
Chris@16
|
435
|
Chris@16
|
436 Command *command = stack.top();
|
Chris@16
|
437 tempStack.push(command);
|
Chris@16
|
438 stack.pop();
|
Chris@16
|
439
|
Chris@17
|
440 QString commandName = command->getName();
|
Chris@16
|
441 commandName.replace(QRegExp("&"), "");
|
Chris@16
|
442
|
Chris@16
|
443 QString text;
|
Chris@16
|
444 if (undo) text = tr("&Undo %1").arg(commandName);
|
Chris@16
|
445 else text = tr("Re&do %1").arg(commandName);
|
Chris@16
|
446
|
Chris@16
|
447 QAction *action = menu->addAction(text);
|
Chris@16
|
448 m_actionCounts[action] = j++;
|
Chris@16
|
449 }
|
Chris@16
|
450
|
Chris@16
|
451 while (!tempStack.empty()) {
|
Chris@16
|
452 stack.push(tempStack.top());
|
Chris@16
|
453 tempStack.pop();
|
Chris@16
|
454 }
|
Chris@16
|
455 }
|
Chris@16
|
456 }
|
Chris@16
|
457
|