View Javadoc

1   /**********************************************
2    * Copyright (C) 2009 Lukas Laag
3    * This file is part of Vectomatic.
4    * 
5    * Vectomatic is free software: you can redistribute it and/or modify
6    * it under the terms of the GNU General Public License as published by
7    * the Free Software Foundation, either version 3 of the License, or
8    * (at your option) any later version.
9    * 
10   * Vectomatic 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
13   * GNU General Public License for more details.
14   * 
15   * You should have received a copy of the GNU General Public License
16   * along with Vectomatic.  If not, see http://www.gnu.org/licenses/
17   **********************************************/
18  package org.vectomatic.client.rep.command;
19  
20  import java.util.ArrayList;
21  import java.util.List;
22  
23  import org.vectomatic.client.rep.events.ICommandHistoryListener;
24  
25  
26  /**
27   * Class to represent an undo/redo stack of commands with a fixed depth.
28   * The oldest command is automatically removed when the stack capacity
29   * is exhausted.
30   * @author Lukas Laag
31   */
32  public class CommandHistory {
33  	/**
34  	 * A circular array to store the commands
35  	 */
36  	private ICommand[] _commands;
37  	
38  	/**
39  	 * The slot in the array occupied by the least recent command
40  	 * Can vary between 0 and K-1 where K is the array capacity
41  	 */
42  	private int _first;
43  
44  	/**
45  	 * The slot in the array occupied by the most recent command
46  	 * Can vary between 0 and K-1 where K is the array capacity
47  	 */
48  	private int _last;
49  
50  	/**
51  	 * The current command
52  	 * Can vary between 0 and N where N is the number of commands
53  	 * in the stack. 0 means before the least recent command (redo
54  	 * is possible, undo is impossible). N means after the most recent
55  	 * command (redo is impossible, undo is possible)
56  	 */
57  	private int _current;
58  	
59  	/**
60  	 * A list of CommandHistoryListener
61  	 */
62  	private List<ICommandHistoryListener> _listeners;
63  	
64  	/**
65  	 * The command at which the last save occurred
66  	 */
67  	private ICommand _savePoint;
68  
69  	/**
70  	 * Constructor
71  	 * @param capacity
72  	 * The depth of the undo stack
73  	 */
74  	public CommandHistory(int capacity) {
75  		if (capacity < 1) {
76  			throw new IllegalArgumentException();
77  		}
78  		_commands = new ICommand[capacity];
79  		_first = -1;
80  	}
81  	
82  	/**
83  	 * Returns true if the command stack contains a command which can be 
84  	 * undone false otherwise.
85  	 * @return true if the command stack contains a command which can be 
86  	 * undone false otherwise.
87  	 */
88  	public boolean canUndo() {
89  		return (_current > 0);
90  	}
91  	
92  	/**
93  	 * Returns true if the command stack contains a command which can be 
94  	 * redone false otherwise.
95  	 * @return true if the command stack contains a command which can be 
96  	 * redone false otherwise.
97  	 */
98  	public boolean canRedo() {
99  		if (_first == -1) {
100 			// Degenerate case: no command in the stack yet
101 			return false;
102 		}
103 		int count = (_last >= _first) ? _last - _first + 1 : _commands.length - _first + _last + 1;
104 		return (_current < count);
105 	}
106 	
107 	/**
108 	 * Undoes the current command
109 	 */
110 	public void undo() {
111 		if (!canUndo()) {
112 			throw new IllegalStateException("Invalid undo");
113 		}
114 		getUndoCommand().unexecute();
115 		_current--;
116 		fireCommandHistoryChange();
117 	}
118 	
119 	/**
120 	 * Redoes the previously undone command
121 	 */
122 	public void redo() {
123 		if (!canRedo()) {
124 			throw new IllegalStateException("Invalid redo");
125 		}
126 		getRedoCommand().execute();
127 		_current++;
128 		fireCommandHistoryChange();
129 	}
130 	
131 	/**
132 	 * Adds a new command to the stack
133 	 * @param command the command to add
134 	 */
135 	public void addCommand(ICommand command) {
136 		if (_first == -1) {
137 			// Degenerate case: no command in the stack yet
138 			_first = 0;
139 		}
140 		
141 		// Remove references to redoable commands
142 		int count = (_last >= _first) ? _last - _first + 1 : _commands.length - _first + _last + 1;
143 		for (int i = _current + 1; i < count; i++) {
144 			int index = (_first + i) % _commands.length;
145 			_commands[index] = null;
146 		}
147 		
148 		// Add new command
149 		_last = (_current + _first) % _commands.length;
150 		_commands[_last] = command;
151 		
152 		// Capacity exhausted: erase oldest command
153 		if (_current == _commands.length) {
154 			_first = (_first == _commands.length - 1) ? 0 : _first + 1;
155 		} else {
156 			// Update current command
157 			_current++;
158 		}
159 		fireCommandHistoryChange();
160 	}
161 	
162 	/**
163 	 * Returns the command which will be undone if undo is invoked
164 	 * @return
165 	 */
166 	public ICommand getUndoCommand() {
167 		if (!canUndo()) {
168 			return null;
169 		}
170 		int index = (_first + _current - 1) % _commands.length;
171 		return _commands[index];
172 	}
173 	
174 	/**
175 	 * Returns the command which will be redone if undo is invoked
176 	 * @return
177 	 */
178 	public ICommand getRedoCommand() {
179 		if (!canRedo()) {
180 			return null;
181 		}
182 		int index = (_first + _current) % _commands.length;
183 		return _commands[index];
184 	}
185 	
186 	/**
187 	 * Gets all the commands currently in the stack
188 	 * @return
189 	 */
190 	public ICommand[] getCommands() {	
191 		if (_first == -1) {
192 			// Degenerate case: no command in the stack yet
193 			return new ICommand[0];
194 		}
195 		int count = (_last >= _first) ? _last - _first + 1 : _commands.length - _first + _last + 1;
196 		ICommand[] commands = new ICommand[count];
197 		for (int i = 0; i < count; i++) {
198 			int index = (_first + i) % _commands.length;
199 			commands[i] = _commands[index];
200 		}
201 		return commands;
202 	}
203 	
204 	/**
205 	 * Returns the index of the current command
206 	 * in the array returned by getCommands().
207 	 * It can vary between 0 and N where N is the number of commands
208 	 * in the stack. 0 means before the least recent command (redo
209 	 * is possible, undo is impossible). N means after the most recent
210 	 * command (redo is impossible, undo is possible)
211 	 * @return
212 	 */
213 	public int getCurrentCommand() {
214 		return _current;
215 	}
216 	
217 	/**
218 	 * Clears the stack
219 	 * @return
220 	 */
221 	public void purge() {
222 		_current = _last = 0;
223 		_first = -1;
224 		for (int i = 0; i < _commands.length; i++) {
225 			_commands[i] = null;
226 		}
227 		_savePoint = null;
228 		fireCommandHistoryChange();
229 	}
230 	
231 	/**
232 	 * Specifies a new capacity for the stack
233 	 * If the new capacity is smaller than the previous
234 	 * one, a proportional window of commands around
235 	 * the current command will be kept.
236 	 * @param capacity
237 	 */
238 	public void setCapacity(int capacity) {
239 		if (_first == -1) {
240 			// Degenerate case: no command in the stack yet
241 			_commands = new ICommand[capacity];
242 			fireCommandHistoryChange();
243 			return;
244 		}		
245 		int count = (_last >= _first) ? _last - _first + 1 : _commands.length - _first + _last + 1;
246 		if (capacity < count) {
247 			int current = _current * capacity / count;
248 			ICommand[] commands = new ICommand[capacity];
249 			for (int i = 0; i < capacity; i++) {
250 				int index = (_first + _current - current + i) % _commands.length;
251 				commands[i] = _commands[index];
252 			}
253 			_commands = commands;
254 			_first = 0;
255 			_current = current;
256 			_last = capacity - 1;
257 			fireCommandHistoryChange();
258 		} else if (capacity > count) {
259 			ICommand[] commands = new ICommand[capacity];
260 			for (int i = 0; i < count; i++) {
261 				int index = (_first + i) % _commands.length;
262 				commands[i] = _commands[index];
263 			}
264 			_commands = commands;
265 			_first = 0;
266 			_last = count - 1;
267 			fireCommandHistoryChange();
268 		}
269 	}
270 	
271 	/**
272 	 * Returns the stack capacity
273 	 * @return
274 	 */
275 	public int getCapacity() {
276 		return _commands.length;
277 	}
278 
279 	public void addCommandHistoryListener(ICommandHistoryListener listener) {
280 		if (_listeners == null) {
281 			_listeners = new ArrayList<ICommandHistoryListener>();
282 		}
283 		_listeners.add(listener);
284 	}
285 
286 	public void removeCommandHistoryListener(ICommandHistoryListener listener) {
287 		if (_listeners != null) {
288 			_listeners.remove(listener);
289 		}
290 	}
291 	
292 	public void fireCommandHistoryChange() {
293 		if (_listeners != null) {
294 			for (int i = 0, size = _listeners.size(); i < size; i++) {
295 				ICommandHistoryListener listener = _listeners.get(i);
296 				listener.commandHistoryChange(this);
297 			}
298 		}
299 	}
300 
301 	public void setSavePoint() {
302 		_savePoint = (canUndo()) ? getUndoCommand() : null;
303 		
304 	}
305 
306 	public boolean needsSaving() {
307 		ICommand command = (canUndo()) ? getUndoCommand() : null;
308 		return (command  != _savePoint);
309 	}
310 
311 }
312