source: josm/trunk/src/org/openstreetmap/josm/gui/dialogs/CommandStackDialog.java@ 4908

Last change on this file since 4908 was 4908, checked in by simon04, 12 years ago

fix #7327 - show hint in undo-menu which action will be undone (from event stack)

  • Property svn:eol-style set to native
File size: 16.5 KB
Line 
1// License: GPL. Copyright 2007 by Immanuel Scholz and others
2package org.openstreetmap.josm.gui.dialogs;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.BorderLayout;
7import java.awt.Component;
8import java.awt.Dimension;
9import java.awt.GridBagLayout;
10import java.awt.Point;
11import java.awt.event.ActionEvent;
12import java.awt.event.KeyEvent;
13import java.awt.event.MouseEvent;
14import java.util.ArrayList;
15import java.util.Arrays;
16import java.util.LinkedHashSet;
17import java.util.List;
18import java.util.Set;
19
20import javax.swing.AbstractAction;
21import javax.swing.Box;
22import javax.swing.JLabel;
23import javax.swing.JPanel;
24import javax.swing.JPopupMenu;
25import javax.swing.JScrollPane;
26import javax.swing.JSeparator;
27import javax.swing.JTree;
28import javax.swing.event.TreeModelEvent;
29import javax.swing.event.TreeModelListener;
30import javax.swing.event.TreeSelectionEvent;
31import javax.swing.event.TreeSelectionListener;
32import javax.swing.tree.DefaultMutableTreeNode;
33import javax.swing.tree.DefaultTreeCellRenderer;
34import javax.swing.tree.DefaultTreeModel;
35import javax.swing.tree.TreePath;
36import javax.swing.tree.TreeSelectionModel;
37
38import org.openstreetmap.josm.Main;
39import org.openstreetmap.josm.command.Command;
40import org.openstreetmap.josm.command.PseudoCommand;
41import org.openstreetmap.josm.data.osm.OsmPrimitive;
42import org.openstreetmap.josm.gui.MapFrame;
43import org.openstreetmap.josm.gui.SideButton;
44import org.openstreetmap.josm.gui.layer.OsmDataLayer;
45import org.openstreetmap.josm.gui.layer.OsmDataLayer.CommandQueueListener;
46import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
47import org.openstreetmap.josm.tools.FilteredCollection;
48import org.openstreetmap.josm.tools.GBC;
49import org.openstreetmap.josm.tools.ImageProvider;
50import org.openstreetmap.josm.tools.Predicate;
51import org.openstreetmap.josm.tools.Shortcut;
52
53public class CommandStackDialog extends ToggleDialog implements CommandQueueListener {
54
55 private DefaultTreeModel undoTreeModel = new DefaultTreeModel(new DefaultMutableTreeNode());
56 private DefaultTreeModel redoTreeModel = new DefaultTreeModel(new DefaultMutableTreeNode());
57
58 private JTree undoTree = new JTree(undoTreeModel);
59 private JTree redoTree = new JTree(redoTreeModel);
60
61 private UndoRedoSelectionListener undoSelectionListener;
62 private UndoRedoSelectionListener redoSelectionListener;
63
64 private JScrollPane scrollPane;
65 private JSeparator separator = new JSeparator();
66 // only visible, if separator is the top most component
67 private Component spacer = Box.createRigidArea(new Dimension(0, 3));
68
69 // last operation is remembered to select the next undo/redo entry in the list
70 // after undo/redo command
71 private UndoRedoType lastOperation = UndoRedoType.UNDO;
72
73 public CommandStackDialog(final MapFrame mapFrame) {
74 super(tr("Command Stack"), "commandstack", tr("Open a list of all commands (undo buffer)."),
75 Shortcut.registerShortcut("subwindow:commandstack", tr("Toggle: {0}", tr("Command Stack")), KeyEvent.VK_O, Shortcut.GROUP_LAYER, Shortcut.SHIFT_DEFAULT), 100, true);
76 undoTree.addMouseListener(new PopupMenuHandler());
77 undoTree.setRootVisible(false);
78 undoTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
79 undoTree.setShowsRootHandles(true);
80 undoTree.expandRow(0);
81 undoTree.setCellRenderer(new CommandCellRenderer());
82 undoSelectionListener = new UndoRedoSelectionListener(undoTree);
83 undoTree.getSelectionModel().addTreeSelectionListener(undoSelectionListener);
84
85 redoTree.addMouseListener(new PopupMenuHandler());
86 redoTree.setRootVisible(false);
87 redoTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
88 redoTree.setShowsRootHandles(true);
89 redoTree.expandRow(0);
90 redoTree.setCellRenderer(new CommandCellRenderer());
91 redoSelectionListener = new UndoRedoSelectionListener(redoTree);
92 redoTree.getSelectionModel().addTreeSelectionListener(redoSelectionListener);
93
94 JPanel treesPanel = new JPanel(new GridBagLayout());
95
96 treesPanel.add(spacer, GBC.eol());
97 spacer.setVisible(false);
98 treesPanel.add(undoTree, GBC.eol().fill(GBC.HORIZONTAL));
99 separator.setVisible(false);
100 treesPanel.add(separator, GBC.eol().fill(GBC.HORIZONTAL));
101 treesPanel.add(redoTree, GBC.eol().fill(GBC.HORIZONTAL));
102 treesPanel.add(Box.createRigidArea(new Dimension(0, 0)), GBC.std().weight(0, 1));
103 treesPanel.setBackground(redoTree.getBackground());
104
105 SelectAction selectAction = new SelectAction();
106 wireUpdateEnabledStateUpdater(selectAction, undoTree);
107 wireUpdateEnabledStateUpdater(selectAction, redoTree);
108
109 UndoRedoAction undoAction = new UndoRedoAction(UndoRedoType.UNDO);
110 wireUpdateEnabledStateUpdater(undoAction, undoTree);
111
112 UndoRedoAction redoAction = new UndoRedoAction(UndoRedoType.REDO);
113 wireUpdateEnabledStateUpdater(redoAction, redoTree);
114
115 scrollPane = (JScrollPane)createLayout(treesPanel, true, Arrays.asList(new SideButton[] {
116 new SideButton(selectAction),
117 new SideButton(undoAction),
118 new SideButton(redoAction)
119 }));
120 }
121
122 private static class CommandCellRenderer extends DefaultTreeCellRenderer {
123 @Override public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) {
124 super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
125 DefaultMutableTreeNode v = (DefaultMutableTreeNode)value;
126 if (v.getUserObject() instanceof JLabel) {
127 JLabel l = (JLabel)v.getUserObject();
128 setIcon(l.getIcon());
129 setText(l.getText());
130 }
131 return this;
132 }
133 }
134
135 /**
136 * Selection listener for undo and redo area.
137 * If one is clicked, takes away the selection from the other, so
138 * it behaves as if it was one component.
139 */
140 private class UndoRedoSelectionListener implements TreeSelectionListener {
141 private JTree source;
142
143 public UndoRedoSelectionListener(JTree source) {
144 this.source = source;
145 }
146
147 public void valueChanged(TreeSelectionEvent e) {
148 if (source == undoTree) {
149 redoTree.getSelectionModel().removeTreeSelectionListener(redoSelectionListener);
150 redoTree.clearSelection();
151 redoTree.getSelectionModel().addTreeSelectionListener(redoSelectionListener);
152 }
153 if (source == redoTree) {
154 undoTree.getSelectionModel().removeTreeSelectionListener(undoSelectionListener);
155 undoTree.clearSelection();
156 undoTree.getSelectionModel().addTreeSelectionListener(undoSelectionListener);
157 }
158 }
159 }
160
161 /**
162 * Interface to provide a callback for enabled state update.
163 */
164 protected interface IEnabledStateUpdating {
165 void updateEnabledState();
166 }
167
168 /**
169 * Wires updater for enabled state to the events.
170 */
171 protected void wireUpdateEnabledStateUpdater(final IEnabledStateUpdating updater, JTree tree) {
172 addShowNotifyListener(updater);
173
174 tree.addTreeSelectionListener(new TreeSelectionListener() {
175 public void valueChanged(TreeSelectionEvent e) {
176 updater.updateEnabledState();
177 }
178 });
179
180 tree.getModel().addTreeModelListener(new TreeModelListener() {
181 public void treeNodesChanged(TreeModelEvent e) {
182 updater.updateEnabledState();
183 }
184
185 public void treeNodesInserted(TreeModelEvent e) {
186 updater.updateEnabledState();
187 }
188
189 public void treeNodesRemoved(TreeModelEvent e) {
190 updater.updateEnabledState();
191 }
192
193 public void treeStructureChanged(TreeModelEvent e) {
194 updater.updateEnabledState();
195 }
196 });
197 }
198
199 @Override
200 public void showNotify() {
201 buildTrees();
202 for (IEnabledStateUpdating listener : showNotifyListener) {
203 listener.updateEnabledState();
204 }
205 Main.main.undoRedo.addCommandQueueListener(this);
206 }
207
208 /**
209 * Simple listener setup to update the button enabled state when the side dialog shows.
210 */
211 Set<IEnabledStateUpdating> showNotifyListener = new LinkedHashSet<IEnabledStateUpdating>();
212
213 private void addShowNotifyListener(IEnabledStateUpdating listener) {
214 showNotifyListener.add(listener);
215 }
216
217 @Override
218 public void hideNotify() {
219 undoTreeModel.setRoot(new DefaultMutableTreeNode());
220 redoTreeModel.setRoot(new DefaultMutableTreeNode());
221 Main.main.undoRedo.removeCommandQueueListener(this);
222 }
223
224 /**
225 * Build the trees of undo and redo commands (initially or when
226 * they have changed).
227 */
228 private void buildTrees() {
229 setTitle(tr("Command Stack"));
230 if (Main.map == null || Main.map.mapView == null || Main.map.mapView.getEditLayer() == null)
231 return;
232
233 List<Command> undoCommands = Main.main.undoRedo.commands;
234 DefaultMutableTreeNode undoRoot = new DefaultMutableTreeNode();
235 for (int i=0; i<undoCommands.size(); ++i) {
236 undoRoot.add(getNodeForCommand(undoCommands.get(i), i));
237 }
238 undoTreeModel.setRoot(undoRoot);
239
240 List<Command> redoCommands = Main.main.undoRedo.redoCommands;
241 DefaultMutableTreeNode redoRoot = new DefaultMutableTreeNode();
242 for (int i=0; i<redoCommands.size(); ++i) {
243 redoRoot.add(getNodeForCommand(redoCommands.get(i), i));
244 }
245 redoTreeModel.setRoot(redoRoot);
246 if (redoTreeModel.getChildCount(redoRoot) > 0) {
247 redoTree.scrollRowToVisible(0);
248 scrollPane.getHorizontalScrollBar().setValue(0);
249 }
250
251 separator.setVisible(!undoCommands.isEmpty() || !redoCommands.isEmpty());
252 spacer.setVisible(undoCommands.isEmpty() && !redoCommands.isEmpty());
253
254 // if one tree is empty, move selection to the other
255 switch (lastOperation) {
256 case UNDO:
257 if (undoCommands.isEmpty()) {
258 lastOperation = UndoRedoType.REDO;
259 }
260 break;
261 case REDO:
262 if (redoCommands.isEmpty()) {
263 lastOperation = UndoRedoType.UNDO;
264 }
265 break;
266 }
267
268 // select the next command to undo/redo
269 switch (lastOperation) {
270 case UNDO:
271 undoTree.setSelectionRow(undoTree.getRowCount()-1);
272 break;
273 case REDO:
274 redoTree.setSelectionRow(0);
275 break;
276 }
277
278 undoTree.scrollRowToVisible(undoTreeModel.getChildCount(undoRoot)-1);
279 scrollPane.getHorizontalScrollBar().setValue(0);
280 }
281
282 /**
283 * Wraps a command in a CommandListMutableTreeNode.
284 * Recursively adds child commands.
285 */
286 protected CommandListMutableTreeNode getNodeForCommand(PseudoCommand c, int idx) {
287 CommandListMutableTreeNode node = new CommandListMutableTreeNode(c, idx);
288 if (c.getChildren() != null) {
289 List<PseudoCommand> children = new ArrayList<PseudoCommand>(c.getChildren());
290 for (int i=0; i<children.size(); ++i) {
291 node.add(getNodeForCommand(children.get(i), i));
292 }
293 }
294 return node;
295 }
296
297 public void commandChanged(int queueSize, int redoSize) {
298 if (!isVisible())
299 return;
300 buildTrees();
301 }
302
303 public class SelectAction extends AbstractAction implements IEnabledStateUpdating {
304
305 public SelectAction() {
306 super();
307 putValue(NAME,tr("Select"));
308 putValue(SHORT_DESCRIPTION, tr("Selects the objects that take part in this command (unless currently deleted)"));
309 putValue(SMALL_ICON, ImageProvider.get("dialogs","select"));
310
311 }
312
313 public void actionPerformed(ActionEvent e) {
314 TreePath path;
315 undoTree.getSelectionPath();
316 if (!undoTree.isSelectionEmpty()) {
317 path = undoTree.getSelectionPath();
318 } else if (!redoTree.isSelectionEmpty()) {
319 path = redoTree.getSelectionPath();
320 } else
321 throw new IllegalStateException();
322
323 if (Main.map == null || Main.map.mapView == null || Main.map.mapView.getEditLayer() == null) return;
324 PseudoCommand c = ((CommandListMutableTreeNode) path.getLastPathComponent()).getCommand();
325
326 final OsmDataLayer currentLayer = Main.map.mapView.getEditLayer();
327
328 FilteredCollection<OsmPrimitive> prims = new FilteredCollection<OsmPrimitive>(
329 c.getParticipatingPrimitives(),
330 new Predicate<OsmPrimitive>(){
331 public boolean evaluate(OsmPrimitive o) {
332 OsmPrimitive p = currentLayer.data.getPrimitiveById(o);
333 return p != null && p.isUsable();
334 }
335 }
336 );
337 Main.map.mapView.getEditLayer().data.setSelected(prims);
338 }
339
340 public void updateEnabledState() {
341 setEnabled(!undoTree.isSelectionEmpty() || !redoTree.isSelectionEmpty());
342 }
343
344 }
345
346 /**
347 * undo / redo switch to reduce duplicate code
348 */
349 protected enum UndoRedoType {UNDO, REDO};
350
351 /**
352 * Action to undo or redo all commands up to (and including) the seleced item.
353 */
354 protected class UndoRedoAction extends AbstractAction implements IEnabledStateUpdating {
355 private UndoRedoType type;
356 private JTree tree;
357
358 /**
359 * constructor
360 * @param type decide whether it is an undo action or a redo action
361 */
362 public UndoRedoAction(UndoRedoType type) {
363 super();
364 this.type = type;
365 switch (type) {
366 case UNDO:
367 tree = undoTree;
368 putValue(NAME,tr("Undo"));
369 putValue(SHORT_DESCRIPTION, tr("Undo the selected and all later commands"));
370 putValue(SMALL_ICON, ImageProvider.get("undo"));
371 break;
372 case REDO:
373 tree = redoTree;
374 putValue(NAME,tr("Redo"));
375 putValue(SHORT_DESCRIPTION, tr("Redo the selected and all earlier commands"));
376 putValue(SMALL_ICON, ImageProvider.get("redo"));
377 break;
378 }
379 }
380
381 public void actionPerformed(ActionEvent e) {
382 lastOperation = type;
383 TreePath path = tree.getSelectionPath();
384
385 // we can only undo top level commands
386 if (path.getPathCount() != 2)
387 throw new IllegalStateException();
388
389 int idx = ((CommandListMutableTreeNode) path.getLastPathComponent()).getIndex();
390
391 // calculate the number of commands to undo/redo; then do it
392 switch (type) {
393 case UNDO:
394 int numUndo = ((DefaultMutableTreeNode) undoTreeModel.getRoot()).getChildCount() - idx;
395 Main.main.undoRedo.undo(numUndo);
396 break;
397 case REDO:
398 int numRedo = idx+1;
399 Main.main.undoRedo.redo(numRedo);
400 break;
401 }
402 Main.map.repaint();
403 }
404
405 public void updateEnabledState() {
406 // do not allow execution if nothing is selected or a sub command was selected
407 setEnabled(!tree.isSelectionEmpty() && tree.getSelectionPath().getPathCount()==2);
408 }
409 }
410
411 class PopupMenuHandler extends PopupMenuLauncher {
412 @Override
413 public void launch(MouseEvent evt) {
414 Point p = evt.getPoint();
415 JTree tree = (JTree) evt.getSource();
416 int row = tree.getRowForLocation(p.x, p.y);
417 if (row != -1) {
418 TreePath path = tree.getPathForLocation(p.x, p.y);
419 // right click on unselected element -> select it first
420 if (!tree.isPathSelected(path)) {
421 tree.setSelectionPath(path);
422 }
423 TreePath[] selPaths = tree.getSelectionPaths();
424
425 CommandStackPopup menu = new CommandStackPopup(selPaths);
426 menu.show(tree, p.x, p.y-3);
427 }
428 }
429 }
430
431 private class CommandStackPopup extends JPopupMenu {
432 private TreePath[] sel;
433 public CommandStackPopup(TreePath[] sel){
434 this.sel = sel;
435 add(new SelectAction());
436 }
437 }
438}
Note: See TracBrowser for help on using the repository browser.