1 |
|
---|
2 | using System;
|
---|
3 | using System.Diagnostics;
|
---|
4 | namespace Netron.Diagramming.Core {
|
---|
5 | /// <summary>
|
---|
6 | /// UndoManager is a concrete class that maintains the undo list
|
---|
7 | /// and redo stack data structures. It also provides methods that
|
---|
8 | /// tell you whether there is something to undo or redo. The class
|
---|
9 | /// is designed to be used directly in undo/redo menu item handlers,
|
---|
10 | /// and undo/redo menu item state update functions.
|
---|
11 | /// </summary>
|
---|
12 | public class UndoManager : IUndoSupport {
|
---|
13 | /// <summary>
|
---|
14 | /// Occurs when the undo/redo history has changed.
|
---|
15 | /// </summary>
|
---|
16 | public event EventHandler OnHistoryChange;
|
---|
17 |
|
---|
18 | #region Fields
|
---|
19 |
|
---|
20 | /// <summary>
|
---|
21 | /// the max level to keep history
|
---|
22 | /// </summary>
|
---|
23 | private int undoLevel;
|
---|
24 | /// <summary>
|
---|
25 | /// the undo list
|
---|
26 | /// </summary>
|
---|
27 | private UndoCollection undoList;
|
---|
28 |
|
---|
29 |
|
---|
30 | /// <summary>
|
---|
31 | /// the internal stack of redoable operations
|
---|
32 | /// </summary>
|
---|
33 | private StackBase<CommandInfo> redoStack;
|
---|
34 |
|
---|
35 | #endregion
|
---|
36 |
|
---|
37 | #region Constructor
|
---|
38 | /// <summary>
|
---|
39 | /// Constructor which initializes the manager with up to 8 levels
|
---|
40 | /// of undo/redo.
|
---|
41 | /// </summary>
|
---|
42 | /// <param name="level">the undo level</param>
|
---|
43 | public UndoManager(int level) {
|
---|
44 | undoLevel = level;
|
---|
45 | undoList = new UndoCollection();
|
---|
46 | redoStack = new StackBase<CommandInfo>();
|
---|
47 | // The following events are linked in the sense that when an item
|
---|
48 | // is popped from the stack it'll be put in the undo collection
|
---|
49 | // again. So, strictly speaking, you need only two of the four
|
---|
50 | // events to make the surface aware of the history changes, but
|
---|
51 | // we'll keep it as is. It depends on your own perception of the
|
---|
52 | // undo/reo process.
|
---|
53 | redoStack.OnItemPopped +=
|
---|
54 | new EventHandler<CollectionEventArgs<CommandInfo>>(
|
---|
55 | HistoryChanged);
|
---|
56 |
|
---|
57 | redoStack.OnItemPushed +=
|
---|
58 | new EventHandler<CollectionEventArgs<CommandInfo>>(
|
---|
59 | HistoryChanged);
|
---|
60 |
|
---|
61 | undoList.OnItemAdded +=
|
---|
62 | new EventHandler<CollectionEventArgs<CommandInfo>>(
|
---|
63 | HistoryChanged);
|
---|
64 |
|
---|
65 | undoList.OnItemRemoved +=
|
---|
66 | new EventHandler<CollectionEventArgs<CommandInfo>>(
|
---|
67 | HistoryChanged);
|
---|
68 |
|
---|
69 | }
|
---|
70 |
|
---|
71 |
|
---|
72 |
|
---|
73 | private void HistoryChanged(object sender, CollectionEventArgs<CommandInfo> e) {
|
---|
74 | RaiseHistoryChange();
|
---|
75 | }
|
---|
76 |
|
---|
77 | #endregion
|
---|
78 |
|
---|
79 | #region Properties
|
---|
80 | /// <summary>
|
---|
81 | /// Property for the maximum undo level.
|
---|
82 | /// </summary>
|
---|
83 | public int MaxUndoLevel {
|
---|
84 | get {
|
---|
85 | return undoLevel;
|
---|
86 | }
|
---|
87 | set {
|
---|
88 | Debug.Assert(value >= 0);
|
---|
89 |
|
---|
90 | // To keep things simple, if you change the undo level,
|
---|
91 | // we clear all outstanding undo/redo commands.
|
---|
92 | if (value != undoLevel) {
|
---|
93 | ClearUndoRedo();
|
---|
94 | undoLevel = value;
|
---|
95 | }
|
---|
96 | }
|
---|
97 | }
|
---|
98 |
|
---|
99 | /// <summary>
|
---|
100 | /// Gets the undo list.
|
---|
101 | /// </summary>
|
---|
102 | /// <value>The undo list.</value>
|
---|
103 | internal UndoCollection UndoList {
|
---|
104 | get { return undoList; }
|
---|
105 | }
|
---|
106 | #endregion
|
---|
107 |
|
---|
108 | #region Methods
|
---|
109 | /// <summary>
|
---|
110 | /// Raises the OnHistoryChange event
|
---|
111 | /// </summary>
|
---|
112 | private void RaiseHistoryChange() {
|
---|
113 | if (OnHistoryChange != null)
|
---|
114 | OnHistoryChange(this, EventArgs.Empty);
|
---|
115 | }
|
---|
116 | /// <summary>
|
---|
117 | /// Register a new undo command. Use this method after your
|
---|
118 | /// application has performed an operation/command that is
|
---|
119 | /// undoable.
|
---|
120 | /// </summary>
|
---|
121 | /// <param name="cmd">New command to add to the manager.</param>
|
---|
122 | public void AddUndoCommand(ICommand cmd) {
|
---|
123 | Debug.Assert(cmd != null);
|
---|
124 | Debug.Assert(undoList.Count <= undoLevel);
|
---|
125 |
|
---|
126 | if (undoLevel == 0)
|
---|
127 | return;
|
---|
128 |
|
---|
129 | CommandInfo info = null;
|
---|
130 | if (undoList.Count == undoLevel) {
|
---|
131 | // Remove the oldest entry from the undo list to make room.
|
---|
132 | info = (CommandInfo)undoList[0];
|
---|
133 | undoList.RemoveAt(0);
|
---|
134 | }
|
---|
135 |
|
---|
136 | // Insert the new undoable command into the undo list.
|
---|
137 | if (info == null)
|
---|
138 | info = new CommandInfo();
|
---|
139 | info.Command = cmd;
|
---|
140 | info.Handler = null;
|
---|
141 | undoList.Add(info);
|
---|
142 |
|
---|
143 | // Clear the redo stack.
|
---|
144 | ClearRedo();
|
---|
145 | }
|
---|
146 |
|
---|
147 | /// <summary>
|
---|
148 | /// Register a new undo command along with an undo handler. The
|
---|
149 | /// undo handler is used to perform the actual undo or redo
|
---|
150 | /// operation later when requested.
|
---|
151 | /// </summary>
|
---|
152 | /// <param name="cmd">New command to add to the manager.</param>
|
---|
153 | /// <param name="undoHandler">Undo handler to perform the actual undo/redo operation.</param>
|
---|
154 | public void AddUndoCommand(ICommand cmd, IUndoSupport undoHandler) {
|
---|
155 | AddUndoCommand(cmd);
|
---|
156 |
|
---|
157 | if (undoList.Count > 0) {
|
---|
158 | CommandInfo info = (CommandInfo)undoList[undoList.Count - 1];
|
---|
159 | Debug.Assert(info != null);
|
---|
160 | info.Handler = undoHandler;
|
---|
161 | }
|
---|
162 | }
|
---|
163 |
|
---|
164 | /// <summary>
|
---|
165 | /// Clear the internal undo/redo data structures. Use this method
|
---|
166 | /// when your application performs an operation that cannot be undone.
|
---|
167 | /// For example, when the user "saves" or "commits" all the changes in
|
---|
168 | /// the application.
|
---|
169 | /// </summary>
|
---|
170 | public void ClearUndoRedo() {
|
---|
171 | ClearUndo();
|
---|
172 | ClearRedo();
|
---|
173 | }
|
---|
174 |
|
---|
175 | /// <summary>
|
---|
176 | /// Check if there is something to undo. Use this method to decide
|
---|
177 | /// whether your application's "Undo" menu item should be enabled
|
---|
178 | /// or disabled.
|
---|
179 | /// </summary>
|
---|
180 | /// <returns>Returns true if there is something to undo, false otherwise.</returns>
|
---|
181 | public bool CanUndo() {
|
---|
182 | return undoList.Count > 0;
|
---|
183 | }
|
---|
184 |
|
---|
185 | /// <summary>
|
---|
186 | /// Check if there is something to redo. Use this method to decide
|
---|
187 | /// whether your application's "Redo" menu item should be enabled
|
---|
188 | /// or disabled.
|
---|
189 | /// </summary>
|
---|
190 | /// <returns>Returns true if there is something to redo, false otherwise.</returns>
|
---|
191 | public bool CanRedo() {
|
---|
192 | return redoStack.Count > 0;
|
---|
193 | }
|
---|
194 |
|
---|
195 | /// <summary>
|
---|
196 | /// Perform the undo operation. If an undo handler is specified, it
|
---|
197 | /// will be used to perform the actual operation. Otherwise, the command
|
---|
198 | /// instance is asked to perform the undo.
|
---|
199 | /// </summary>
|
---|
200 | public void Undo() {
|
---|
201 | if (!CanUndo())
|
---|
202 | return;
|
---|
203 |
|
---|
204 | // Remove newest entry from the undo list.
|
---|
205 | CommandInfo info = (CommandInfo)undoList[undoList.Count - 1];
|
---|
206 | undoList.RemoveAt(undoList.Count - 1);
|
---|
207 |
|
---|
208 | // Perform the undo.
|
---|
209 | Debug.Assert(info.Command != null);
|
---|
210 |
|
---|
211 | info.Command.Undo();
|
---|
212 |
|
---|
213 | // Now the command is available for redo. Push it onto
|
---|
214 | // the redo stack.
|
---|
215 | redoStack.Push(info);
|
---|
216 | }
|
---|
217 |
|
---|
218 | /// <summary>
|
---|
219 | /// Perform the redo operation. If an undo handler is specified, it
|
---|
220 | /// will be used to perform the actual operation. Otherwise, the command
|
---|
221 | /// instance is asked to perform the redo.
|
---|
222 | /// </summary>
|
---|
223 | public void Redo() {
|
---|
224 | if (!CanRedo())
|
---|
225 | return;
|
---|
226 |
|
---|
227 | // Remove newest entry from the redo stack.
|
---|
228 | CommandInfo info = (CommandInfo)redoStack.Pop();
|
---|
229 |
|
---|
230 | // Perform the redo.
|
---|
231 | Debug.Assert(info.Command != null);
|
---|
232 |
|
---|
233 | info.Command.Redo();
|
---|
234 |
|
---|
235 | // Now the command is available for undo again. Put it back
|
---|
236 | // into the undo list.
|
---|
237 | undoList.Add(info);
|
---|
238 | }
|
---|
239 |
|
---|
240 | /// <summary>
|
---|
241 | /// Get the text value of the next undo command. Use this method
|
---|
242 | /// to update the Text property of your "Undo" menu item if
|
---|
243 | /// desired. For example, the text value for a command might be
|
---|
244 | /// "Draw Circle". This allows you to change your menu item Text
|
---|
245 | /// property to "Undo Draw Circle".
|
---|
246 | /// </summary>
|
---|
247 | /// <returns>Text value of the next undo command.</returns>
|
---|
248 | public string UndoText {
|
---|
249 | get {
|
---|
250 | ICommand cmd = NextUndoCommand;
|
---|
251 | if (cmd == null)
|
---|
252 | return "";
|
---|
253 | return cmd.Text;
|
---|
254 | }
|
---|
255 | }
|
---|
256 |
|
---|
257 | /// <summary>
|
---|
258 | /// <para>
|
---|
259 | /// Get the text value of the next redo command. Use this method
|
---|
260 | /// to update the Text property of your "Redo" menu item if desired.
|
---|
261 | /// For example, the text value for a command might be "Draw Line".
|
---|
262 | /// This allows you to change your menu item text to "Redo Draw Line".
|
---|
263 | /// </para>
|
---|
264 | /// </summary>
|
---|
265 | /// <value>The redo text.</value>
|
---|
266 | /// <returns>Text value of the next redo command.</returns>
|
---|
267 | public string RedoText {
|
---|
268 | get {
|
---|
269 | ICommand cmd = NextRedoCommand;
|
---|
270 | if (cmd == null)
|
---|
271 | return "";
|
---|
272 | return cmd.Text;
|
---|
273 | }
|
---|
274 | }
|
---|
275 |
|
---|
276 | /// <summary>
|
---|
277 | /// Get the next (or newest) undo command. This is like a "Peek"
|
---|
278 | /// method. It does not remove the command from the undo list.
|
---|
279 | /// </summary>
|
---|
280 | /// <returns>The next undo command.</returns>
|
---|
281 | public ICommand NextUndoCommand {
|
---|
282 | get {
|
---|
283 | if (undoList.Count == 0)
|
---|
284 | return null;
|
---|
285 | CommandInfo info = (CommandInfo)undoList[undoList.Count - 1];
|
---|
286 | return info.Command;
|
---|
287 | }
|
---|
288 | }
|
---|
289 |
|
---|
290 | /// <summary>
|
---|
291 | /// Get the next redo command. This is like a "Peek"
|
---|
292 | /// method. It does not remove the command from the redo stack.
|
---|
293 | /// </summary>
|
---|
294 | /// <returns>The next redo command.</returns>
|
---|
295 | public ICommand NextRedoCommand {
|
---|
296 | get {
|
---|
297 | if (redoStack.Count == 0)
|
---|
298 | return null;
|
---|
299 | CommandInfo info = (CommandInfo)redoStack.Peek();
|
---|
300 | return info.Command;
|
---|
301 | }
|
---|
302 |
|
---|
303 | }
|
---|
304 |
|
---|
305 | /// <summary>
|
---|
306 | /// Retrieve all of the undo commands. Useful for debugging,
|
---|
307 | /// to analyze the contents of the undo list.
|
---|
308 | /// </summary>
|
---|
309 | /// <returns>Array of commands for undo.</returns>
|
---|
310 | public ICommand[] GetUndoCommands() {
|
---|
311 | if (undoList.Count == 0)
|
---|
312 | return null;
|
---|
313 |
|
---|
314 | ICommand[] cmdList = new ICommand[undoList.Count];
|
---|
315 | object[] objList = undoList.ToArray();
|
---|
316 | for (int i = 0; i < objList.Length; i++) {
|
---|
317 | CommandInfo info = (CommandInfo)objList[i];
|
---|
318 | cmdList[i] = info.Command;
|
---|
319 | }
|
---|
320 |
|
---|
321 | return cmdList;
|
---|
322 | }
|
---|
323 |
|
---|
324 | /// <summary>
|
---|
325 | /// Retrieve all of the redo commands. Useful for debugging,
|
---|
326 | /// to analyze the contents of the redo stack.
|
---|
327 | /// </summary>
|
---|
328 | /// <returns>Array of commands for redo.</returns>
|
---|
329 | public ICommand[] GetRedoCommands() {
|
---|
330 | if (redoStack.Count == 0)
|
---|
331 | return null;
|
---|
332 |
|
---|
333 | ICommand[] cmdList = new ICommand[redoStack.Count];
|
---|
334 | object[] objList = redoStack.ToArray();
|
---|
335 | for (int i = 0; i < objList.Length; i++) {
|
---|
336 | CommandInfo info = (CommandInfo)objList[i];
|
---|
337 | cmdList[i] = info.Command;
|
---|
338 | }
|
---|
339 |
|
---|
340 | return cmdList;
|
---|
341 | }
|
---|
342 |
|
---|
343 | /// <summary>
|
---|
344 | /// Clear the contents of the undo list.
|
---|
345 | /// </summary>
|
---|
346 | private void ClearUndo() {
|
---|
347 | while (undoList.Count > 0) {
|
---|
348 | CommandInfo info = (CommandInfo)undoList[undoList.Count - 1];
|
---|
349 | undoList.RemoveAt(undoList.Count - 1);
|
---|
350 | info.Command = null;
|
---|
351 | info.Handler = null;
|
---|
352 | }
|
---|
353 | }
|
---|
354 |
|
---|
355 | /// <summary>
|
---|
356 | /// Clear the contents of the redo stack.
|
---|
357 | /// </summary>
|
---|
358 | private void ClearRedo() {
|
---|
359 | while (redoStack.Count > 0) {
|
---|
360 | CommandInfo info = (CommandInfo)redoStack.Pop();
|
---|
361 | info.Command = null;
|
---|
362 | info.Handler = null;
|
---|
363 | }
|
---|
364 | }
|
---|
365 | #endregion
|
---|
366 | }
|
---|
367 |
|
---|
368 | class UndoCollection : CollectionBase<CommandInfo> {
|
---|
369 |
|
---|
370 | }
|
---|
371 | }
|
---|
372 |
|
---|
373 |
|
---|