1/* This file is part of the KDE project
2 Copyright (C) 2000 Werner Trobin <trobin@kde.org>
3 Copyright (C) 2000,2006 David Faure <faure@kde.org>
4
5 This library is free software; you can redistribute it and/or
6 modify it under the terms of the GNU Library General Public
7 License as published by the Free Software Foundation; either
8 version 2 of the License, or (at your option) any later version.
9
10 This library is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 Library General Public License for more details.
14
15 You should have received a copy of the GNU Library General Public License
16 along with this library; see the file COPYING.LIB. If not, write to
17 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
18 Boston, MA 02110-1301, USA.
19*/
20
21#include "k3command.h"
22#include <kaction.h>
23#include <kactioncollection.h>
24#include <kstandardshortcut.h>
25#include <kstandardaction.h>
26#include <kdebug.h>
27#include <kicon.h>
28#include <klocale.h>
29#include <kmenu.h>
30
31#include "ktoolbarpopupaction.h"
32
33K3Command::K3Command()
34 : d( 0 )
35{
36}
37
38K3Command::~K3Command()
39{
40}
41
42class K3NamedCommand::Private
43{
44 public:
45 QString name;
46};
47
48K3NamedCommand::K3NamedCommand( const QString &name )
49 : K3Command(),
50 d( new Private )
51{
52 d->name = name;
53}
54
55K3NamedCommand::~K3NamedCommand()
56{
57 delete d;
58}
59
60QString K3NamedCommand::name() const
61{
62 return d->name;
63}
64
65void K3NamedCommand::setName( const QString &name )
66{
67 d->name = name;
68}
69
70class K3MacroCommand::Private
71{
72 public:
73 QList<K3Command *> commands;
74};
75
76K3MacroCommand::K3MacroCommand( const QString & name )
77 : K3NamedCommand(name),
78 d( new Private )
79{
80}
81
82K3MacroCommand::~K3MacroCommand()
83{
84 qDeleteAll( d->commands );
85 delete d;
86}
87
88void K3MacroCommand::addCommand( K3Command *command )
89{
90 d->commands.append(command);
91}
92
93void K3MacroCommand::execute()
94{
95 QListIterator<K3Command *> it( d->commands );
96 while ( it.hasNext() ) {
97 it.next()->execute();
98 }
99}
100
101void K3MacroCommand::unexecute()
102{
103 QListIterator<K3Command *> it( d->commands );
104 it.toBack();
105 while ( it.hasPrevious() ) {
106 it.previous()->unexecute();
107 }
108}
109
110const QList<K3Command *> K3MacroCommand::commands() const
111{
112 return d->commands;
113}
114
115
116class K3CommandHistory::K3CommandHistoryPrivate {
117public:
118 K3CommandHistoryPrivate()
119 : m_undoLimit(50), m_redoLimit(30),
120 m_savedAt(-1), m_current(-1) {
121 }
122 ~K3CommandHistoryPrivate() {
123 qDeleteAll( m_commands );
124 }
125
126 QList<K3Command *> m_commands;
127 int m_undoLimit, m_redoLimit;
128
129 int m_savedAt;
130 int m_current;
131 /*
132 If m_commands contains: <c0> <c1> <c2> <c3>
133
134 m_current = 1 means we are between <c1> and <c2>, i.e. undo would unexecute c1.
135 So m_current is the index of the current undo command, m_current+1 the current redo command if any.
136
137 Adding a command at this point would delete <c2> and <c3>.
138 m_current compared to the commands: -1 <c0> 0 <c1> 1 <c2> 2.
139
140 m_savedAt = 1 means that we where at m_current == 1 when the document was saved.
141 m_savedAt = -1 means that the document was saved with an empty history (initial state, too).
142 m_savedAt = -2 means that the document wasn't saved in the current visible history
143 (this happens when the undo history got truncated)
144 */
145};
146
147////////////
148
149K3CommandHistory::K3CommandHistory() :
150 d( new K3CommandHistoryPrivate )
151{
152 clear();
153}
154
155K3CommandHistory::K3CommandHistory(KActionCollection * actionCollection, bool withMenus) :
156 d( new K3CommandHistoryPrivate )
157{
158 if (withMenus)
159 {
160 // TODO instead of a menu this should show a listbox like koffice's KoCommandHistory does,
161 // so that it's clearer that N actions will be undone together, not just action number N.
162
163 // TODO also move this out of K3CommandHistory, to make it core-only.
164
165 new K3UndoRedoAction( K3UndoRedoAction::Undo, actionCollection, this );
166 new K3UndoRedoAction( K3UndoRedoAction::Redo, actionCollection, this );
167 }
168 else
169 {
170 actionCollection->addAction(KStandardAction::Undo, this, SLOT(undo()));
171 actionCollection->addAction(KStandardAction::Redo, this, SLOT(redo()));
172 }
173 clear();
174}
175
176K3CommandHistory::~K3CommandHistory() {
177 delete d;
178}
179
180void K3CommandHistory::clear() {
181 qDeleteAll( d->m_commands );
182 d->m_commands.clear();
183 d->m_current = -1;
184 d->m_savedAt = -1;
185 emit commandHistoryChanged();
186}
187
188void K3CommandHistory::addCommand(K3Command *command, bool execute) {
189 if ( !command )
190 return;
191
192 ++d->m_current;
193 d->m_commands.insert( d->m_current, command );
194 // Truncate history
195 int count = d->m_commands.count();
196 for ( int i = d->m_current + 1; i < count; ++i )
197 delete d->m_commands.takeLast();
198
199 // Check whether we still can reach savedAt
200 if ( d->m_current < d->m_savedAt )
201 d->m_savedAt = -2;
202
203 clipCommands();
204
205 if ( execute )
206 {
207 command->execute();
208 emit commandExecuted(command);
209 }
210}
211
212K3Command * K3CommandHistory::presentCommand() const
213{
214 if ( d->m_current >= 0 )
215 return d->m_commands[ d->m_current ];
216 return 0;
217}
218
219void K3CommandHistory::undo() {
220 Q_ASSERT( d->m_current >= 0 );
221
222 K3Command* command = d->m_commands[ d->m_current ];
223
224 command->unexecute();
225 emit commandExecuted( command );
226
227 --d->m_current;
228
229 if ( d->m_current == d->m_savedAt )
230 emit documentRestored();
231
232 clipCommands(); // only needed here and in addCommand, NOT in redo
233}
234
235void K3CommandHistory::redo() {
236 K3Command* command = d->m_commands[ d->m_current + 1 ];
237 command->execute();
238 emit commandExecuted( command );
239
240 ++d->m_current;
241
242 if ( d->m_current == d->m_savedAt )
243 emit documentRestored();
244
245 emit commandHistoryChanged();
246}
247
248void K3CommandHistory::documentSaved() {
249 d->m_savedAt = d->m_current;
250}
251
252void K3CommandHistory::setUndoLimit(int limit) {
253 if ( limit>0 && limit != d->m_undoLimit ) {
254 d->m_undoLimit = limit;
255 clipCommands();
256 }
257}
258
259void K3CommandHistory::setRedoLimit(int limit) {
260 if ( limit>0 && limit != d->m_redoLimit ) {
261 d->m_redoLimit = limit;
262 clipCommands();
263 }
264}
265
266void K3CommandHistory::clipCommands() {
267 int count = d->m_commands.count();
268 if ( count <= d->m_undoLimit && count <= d->m_redoLimit ) {
269 emit commandHistoryChanged();
270 return;
271 }
272
273 if ( d->m_current >= d->m_undoLimit ) {
274 const int toRemove = (d->m_current - d->m_undoLimit) + 1;
275 for ( int i = 0; i < toRemove; ++i ) {
276 delete d->m_commands.takeFirst();
277 --d->m_savedAt;
278 --d->m_current;
279 }
280 Q_ASSERT( d->m_current >= -1 );
281 count = d->m_commands.count(); // update count for the redo branch below
282 if ( d->m_savedAt < 0 )
283 d->m_savedAt = -1; // savedAt went out of the history
284 }
285
286 if ( d->m_current + d->m_redoLimit + 1 < count ) {
287 if ( d->m_savedAt > (d->m_current + d->m_redoLimit) )
288 d->m_savedAt = -1;
289 const int toRemove = count - (d->m_current + d->m_redoLimit + 1);
290 for ( int i = 0; i< toRemove; ++i )
291 delete d->m_commands.takeLast();
292 }
293 emit commandHistoryChanged();
294}
295
296void K3CommandHistory::updateActions()
297{
298 // it hasn't changed, but this updates all actions connected to this command history.
299 emit commandHistoryChanged();
300}
301
302bool K3CommandHistory::isUndoAvailable() const
303{
304 return d->m_current >= 0;
305}
306
307bool K3CommandHistory::isRedoAvailable() const
308{
309 return d->m_current < d->m_commands.count() - 1;
310}
311
312QList<K3Command *> K3CommandHistory::undoCommands( int maxCommands ) const
313{
314 QList<K3Command *> lst;
315 for ( int i = d->m_current; i >= 0; --i ) {
316 lst.append( d->m_commands[i] );
317 if ( maxCommands > 0 && lst.count() == maxCommands )
318 break;
319 }
320 return lst;
321}
322
323QList<K3Command *> K3CommandHistory::redoCommands( int maxCommands ) const
324{
325 QList<K3Command *> lst;
326 for ( int i = d->m_current + 1; i < d->m_commands.count(); ++i )
327 {
328 lst.append( d->m_commands[i] );
329 if ( maxCommands > 0 && lst.count() == maxCommands )
330 break;
331 }
332 return lst;
333}
334
335int K3CommandHistory::undoLimit() const
336{
337 return d->m_undoLimit;
338}
339
340int K3CommandHistory::redoLimit() const
341{
342 return d->m_redoLimit;
343}
344
345class K3UndoRedoAction::Private
346{
347 public:
348 Private( K3UndoRedoAction::Type type, K3CommandHistory* commandHistory)
349 : type( type ),
350 commandHistory( commandHistory )
351 {
352 }
353
354 Type type;
355 K3CommandHistory* commandHistory;
356};
357
358
359
360K3UndoRedoAction::K3UndoRedoAction( Type type, KActionCollection* actionCollection, K3CommandHistory* commandHistory )
361 : KToolBarPopupAction( KIcon( type == Undo ? "edit-undo" : "edit-redo" ),
362 QString(), // text is set in clear() on start
363 actionCollection),
364 d( new Private( type, commandHistory ) )
365{
366 setShortcut( KStandardShortcut::shortcut( type == Undo ? KStandardShortcut::Undo : KStandardShortcut::Redo ) );
367 if ( d->type == Undo ) {
368 connect( this, SIGNAL(triggered(bool)), d->commandHistory, SLOT(undo()) );
369 } else {
370 connect( this, SIGNAL(triggered(bool)), d->commandHistory, SLOT(redo()) );
371 }
372 connect( this->menu(), SIGNAL(aboutToShow()), this, SLOT(slotAboutToShow()) );
373 connect( this->menu(), SIGNAL(triggered(QAction*)), this, SLOT(slotActionTriggered(QAction*)) );
374
375 connect( d->commandHistory, SIGNAL(commandHistoryChanged()), this, SLOT(slotCommandHistoryChanged()) );
376 slotCommandHistoryChanged();
377 actionCollection->addAction(KStandardAction::name(type == Undo ? KStandardAction::Undo : KStandardAction::Redo),
378 this);
379}
380
381void K3UndoRedoAction::slotAboutToShow()
382{
383 menu()->clear();
384 // TODO make number of items configurable ?
385 const int maxCommands = 9;
386 if ( d->type == Undo ) {
387 const QList<K3Command *> commands = d->commandHistory->undoCommands( maxCommands );
388 for (int i = 0; i < commands.count(); ++i) {
389 QAction *action = menu()->addAction( i18n("Undo: %1", commands[i]->name()) );
390 action->setData( i );
391 }
392 } else {
393 const QList<K3Command *> commands = d->commandHistory->redoCommands( maxCommands );
394 for (int i = 0; i < commands.count(); ++i) {
395 QAction *action = menu()->addAction( i18n("Redo: %1", commands[i]->name()) );
396 action->setData( i );
397 }
398 }
399}
400
401void K3UndoRedoAction::slotActionTriggered( QAction *action )
402{
403 const int pos = action->data().toInt();
404 //kDebug(230) << pos;
405 if ( d->type == Undo ) {
406 for ( int i = 0 ; i < pos+1; ++i ) {
407 d->commandHistory->undo();
408 }
409 } else {
410 for ( int i = 0 ; i < pos+1; ++i ) {
411 d->commandHistory->redo();
412 }
413 }
414}
415
416void K3UndoRedoAction::slotCommandHistoryChanged()
417{
418 const bool isUndo = d->type == Undo;
419 const bool enabled = isUndo ? d->commandHistory->isUndoAvailable() : d->commandHistory->isRedoAvailable();
420 setEnabled(enabled);
421 if (!enabled) {
422 setText(isUndo ? i18n("&Undo") : i18n("&Redo"));
423 } else {
424 if (isUndo) {
425 K3Command* presentCommand = d->commandHistory->presentCommand();
426 Q_ASSERT(presentCommand);
427 setText(i18n("&Undo: %1", presentCommand->name()));
428 } else {
429 K3Command* redoCommand = d->commandHistory->redoCommands(1).first();
430 setText(i18n("&Redo: %1", redoCommand->name()));
431 }
432 }
433}
434
435
436void K3Command::virtual_hook( int, void* )
437{ /*BASE::virtual_hook( id, data );*/ }
438
439void K3NamedCommand::virtual_hook( int id, void* data )
440{ K3Command::virtual_hook( id, data ); }
441
442void K3MacroCommand::virtual_hook( int id, void* data )
443{ K3NamedCommand::virtual_hook( id, data ); }
444
445#include "k3command.moc"
446