001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data; 003 004import java.util.Collection; 005import java.util.Iterator; 006import java.util.LinkedList; 007 008import org.openstreetmap.josm.Main; 009import org.openstreetmap.josm.command.Command; 010import org.openstreetmap.josm.data.osm.DataSet; 011import org.openstreetmap.josm.data.osm.OsmPrimitive; 012import org.openstreetmap.josm.gui.layer.Layer; 013import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent; 014import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener; 015import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent; 016import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent; 017import org.openstreetmap.josm.gui.layer.OsmDataLayer; 018import org.openstreetmap.josm.gui.layer.OsmDataLayer.CommandQueueListener; 019import org.openstreetmap.josm.tools.CheckParameterUtil; 020 021/** 022 * This is the global undo/redo handler for all {@link OsmDataLayer}s. 023 * <p> 024 * If you want to change a data layer, you can use {@link #add(Command)} to execute a command on it and make that command undoable. 025 */ 026public class UndoRedoHandler implements LayerChangeListener { 027 028 /** 029 * All commands that were made on the dataset. Don't write from outside! 030 */ 031 public final LinkedList<Command> commands = new LinkedList<>(); 032 /** 033 * The stack for redoing commands 034 */ 035 public final LinkedList<Command> redoCommands = new LinkedList<>(); 036 037 private final LinkedList<CommandQueueListener> listenerCommands = new LinkedList<>(); 038 039 /** 040 * Constructs a new {@code UndoRedoHandler}. 041 */ 042 public UndoRedoHandler() { 043 Main.getLayerManager().addLayerChangeListener(this); 044 } 045 046 /** 047 * Executes the command and add it to the intern command queue. 048 * @param c The command to execute. Must not be {@code null}. 049 */ 050 public void addNoRedraw(final Command c) { 051 CheckParameterUtil.ensureParameterNotNull(c, "c"); 052 c.executeCommand(); 053 c.invalidateAffectedLayers(); 054 commands.add(c); 055 // Limit the number of commands in the undo list. 056 // Currently you have to undo the commands one by one. If 057 // this changes, a higher default value may be reasonable. 058 if (commands.size() > Main.pref.getInteger("undo.max", 1000)) { 059 commands.removeFirst(); 060 } 061 redoCommands.clear(); 062 } 063 064 /** 065 * Fires a commands change event after adding a command. 066 */ 067 public void afterAdd() { 068 fireCommandsChanged(); 069 } 070 071 /** 072 * Executes the command and add it to the intern command queue. 073 * @param c The command to execute. Must not be {@code null}. 074 */ 075 public synchronized void add(final Command c) { 076 DataSet ds = c.getAffectedDataSet(); 077 if (ds == null) { 078 // old, legacy behaviour 079 ds = Main.getLayerManager().getEditDataSet(); 080 } 081 Collection<? extends OsmPrimitive> oldSelection = null; 082 if (ds != null) { 083 oldSelection = ds.getSelected(); 084 } 085 addNoRedraw(c); 086 afterAdd(); 087 088 // the command may have changed the selection so tell the listeners about the current situation 089 if (ds != null) { 090 fireIfSelectionChanged(ds, oldSelection); 091 } 092 } 093 094 /** 095 * Undoes the last added command. 096 */ 097 public void undo() { 098 undo(1); 099 } 100 101 /** 102 * Undoes multiple commands. 103 * @param num The number of commands to undo 104 */ 105 public synchronized void undo(int num) { 106 if (commands.isEmpty()) 107 return; 108 DataSet ds = Main.getLayerManager().getEditDataSet(); 109 Collection<? extends OsmPrimitive> oldSelection = null; 110 if (ds != null) { 111 oldSelection = ds.getSelected(); 112 ds.beginUpdate(); 113 } 114 try { 115 for (int i = 1; i <= num; ++i) { 116 final Command c = commands.removeLast(); 117 c.undoCommand(); 118 c.invalidateAffectedLayers(); 119 redoCommands.addFirst(c); 120 if (commands.isEmpty()) { 121 break; 122 } 123 } 124 } finally { 125 if (ds != null) { 126 ds.endUpdate(); 127 } 128 } 129 fireCommandsChanged(); 130 if (ds != null) { 131 fireIfSelectionChanged(ds, oldSelection); 132 } 133 } 134 135 /** 136 * Redoes the last undoed command. 137 */ 138 public void redo() { 139 redo(1); 140 } 141 142 /** 143 * Redoes multiple commands. 144 * @param num The number of commands to redo 145 */ 146 public void redo(int num) { 147 if (redoCommands.isEmpty()) 148 return; 149 DataSet ds = Main.getLayerManager().getEditDataSet(); 150 Collection<? extends OsmPrimitive> oldSelection = ds.getSelected(); 151 for (int i = 0; i < num; ++i) { 152 final Command c = redoCommands.removeFirst(); 153 c.executeCommand(); 154 c.invalidateAffectedLayers(); 155 commands.add(c); 156 if (redoCommands.isEmpty()) { 157 break; 158 } 159 } 160 fireCommandsChanged(); 161 fireIfSelectionChanged(ds, oldSelection); 162 } 163 164 private static void fireIfSelectionChanged(DataSet ds, Collection<? extends OsmPrimitive> oldSelection) { 165 Collection<? extends OsmPrimitive> newSelection = ds.getSelected(); 166 if (!oldSelection.equals(newSelection)) { 167 ds.fireSelectionChanged(); 168 } 169 } 170 171 /** 172 * Fires a command change to all listeners. 173 */ 174 private void fireCommandsChanged() { 175 for (final CommandQueueListener l : listenerCommands) { 176 l.commandChanged(commands.size(), redoCommands.size()); 177 } 178 } 179 180 /** 181 * Resets the undo/redo list. 182 */ 183 public void clean() { 184 redoCommands.clear(); 185 commands.clear(); 186 fireCommandsChanged(); 187 } 188 189 /** 190 * Resets all commands that affect the given layer. 191 * @param layer The layer that was affected. 192 */ 193 public void clean(Layer layer) { 194 if (layer == null) 195 return; 196 boolean changed = false; 197 for (Iterator<Command> it = commands.iterator(); it.hasNext();) { 198 if (it.next().invalidBecauselayerRemoved(layer)) { 199 it.remove(); 200 changed = true; 201 } 202 } 203 for (Iterator<Command> it = redoCommands.iterator(); it.hasNext();) { 204 if (it.next().invalidBecauselayerRemoved(layer)) { 205 it.remove(); 206 changed = true; 207 } 208 } 209 if (changed) { 210 fireCommandsChanged(); 211 } 212 } 213 214 @Override 215 public void layerRemoving(LayerRemoveEvent e) { 216 clean(e.getRemovedLayer()); 217 } 218 219 @Override 220 public void layerAdded(LayerAddEvent e) { 221 // Do nothing 222 } 223 224 @Override 225 public void layerOrderChanged(LayerOrderChangeEvent e) { 226 // Do nothing 227 } 228 229 /** 230 * Removes a command queue listener. 231 * @param l The command queue listener to remove 232 */ 233 public void removeCommandQueueListener(CommandQueueListener l) { 234 listenerCommands.remove(l); 235 } 236 237 /** 238 * Adds a command queue listener. 239 * @param l The commands queue listener to add 240 * @return {@code true} if the listener has been added, {@code false} otherwise 241 */ 242 public boolean addCommandQueueListener(CommandQueueListener l) { 243 return listenerCommands.add(l); 244 } 245}