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

Last change on this file since 13943 was 13943, checked in by Don-vip, 6 years ago

fix #16389 - fix wrong undo/redo behaviour in command stack dialog (regression from r13729)

  • Property svn:eol-style set to native
File size: 20.0 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.dialogs;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.Component;
7import java.awt.Dimension;
8import java.awt.GridBagLayout;
9import java.awt.event.ActionEvent;
10import java.awt.event.KeyEvent;
11import java.awt.event.MouseEvent;
12import java.util.ArrayList;
13import java.util.Arrays;
14import java.util.Collection;
15import java.util.LinkedHashSet;
16import java.util.List;
17import java.util.Set;
18
19import javax.swing.AbstractAction;
20import javax.swing.Box;
21import javax.swing.JComponent;
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.MutableTreeNode;
36import javax.swing.tree.TreePath;
37import javax.swing.tree.TreeSelectionModel;
38
39import org.openstreetmap.josm.actions.AutoScaleAction;
40import org.openstreetmap.josm.command.Command;
41import org.openstreetmap.josm.command.PseudoCommand;
42import org.openstreetmap.josm.data.UndoRedoHandler.CommandAddedEvent;
43import org.openstreetmap.josm.data.UndoRedoHandler.CommandQueueCleanedEvent;
44import org.openstreetmap.josm.data.UndoRedoHandler.CommandQueuePreciseListener;
45import org.openstreetmap.josm.data.UndoRedoHandler.CommandRedoneEvent;
46import org.openstreetmap.josm.data.UndoRedoHandler.CommandUndoneEvent;
47import org.openstreetmap.josm.data.osm.DataSet;
48import org.openstreetmap.josm.data.osm.OsmPrimitive;
49import org.openstreetmap.josm.gui.MainApplication;
50import org.openstreetmap.josm.gui.SideButton;
51import org.openstreetmap.josm.gui.layer.OsmDataLayer;
52import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
53import org.openstreetmap.josm.tools.GBC;
54import org.openstreetmap.josm.tools.ImageProvider;
55import org.openstreetmap.josm.tools.InputMapUtils;
56import org.openstreetmap.josm.tools.Shortcut;
57import org.openstreetmap.josm.tools.SubclassFilteredCollection;
58
59/**
60 * Dialog displaying list of all executed commands (undo/redo buffer).
61 * @since 94
62 */
63public class CommandStackDialog extends ToggleDialog implements CommandQueuePreciseListener {
64
65 private final DefaultTreeModel undoTreeModel = new DefaultTreeModel(new DefaultMutableTreeNode());
66 private final DefaultTreeModel redoTreeModel = new DefaultTreeModel(new DefaultMutableTreeNode());
67
68 private final JTree undoTree = new JTree(undoTreeModel);
69 private final JTree redoTree = new JTree(redoTreeModel);
70
71 private DefaultMutableTreeNode undoRoot;
72 private DefaultMutableTreeNode redoRoot;
73
74 private final transient UndoRedoSelectionListener undoSelectionListener;
75 private final transient UndoRedoSelectionListener redoSelectionListener;
76
77 private final JScrollPane scrollPane;
78 private final JSeparator separator = new JSeparator();
79 // only visible, if separator is the top most component
80 private final Component spacer = Box.createRigidArea(new Dimension(0, 3));
81
82 // last operation is remembered to select the next undo/redo entry in the list
83 // after undo/redo command
84 private UndoRedoType lastOperation = UndoRedoType.UNDO;
85
86 // Actions for context menu and Enter key
87 private final SelectAction selectAction = new SelectAction();
88 private final SelectAndZoomAction selectAndZoomAction = new SelectAndZoomAction();
89
90 /**
91 * Constructs a new {@code CommandStackDialog}.
92 */
93 public CommandStackDialog() {
94 super(tr("Command Stack"), "commandstack", tr("Open a list of all commands (undo buffer)."),
95 Shortcut.registerShortcut("subwindow:commandstack", tr("Toggle: {0}",
96 tr("Command Stack")), KeyEvent.VK_O, Shortcut.ALT_SHIFT), 100);
97 undoTree.addMouseListener(new MouseEventHandler());
98 undoTree.setRootVisible(false);
99 undoTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
100 undoTree.setShowsRootHandles(true);
101 undoTree.expandRow(0);
102 undoTree.setCellRenderer(new CommandCellRenderer());
103 undoSelectionListener = new UndoRedoSelectionListener(undoTree);
104 undoTree.getSelectionModel().addTreeSelectionListener(undoSelectionListener);
105 InputMapUtils.unassignCtrlShiftUpDown(undoTree, JComponent.WHEN_FOCUSED);
106
107 redoTree.addMouseListener(new MouseEventHandler());
108 redoTree.setRootVisible(false);
109 redoTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
110 redoTree.setShowsRootHandles(true);
111 redoTree.expandRow(0);
112 redoTree.setCellRenderer(new CommandCellRenderer());
113 redoSelectionListener = new UndoRedoSelectionListener(redoTree);
114 redoTree.getSelectionModel().addTreeSelectionListener(redoSelectionListener);
115 InputMapUtils.unassignCtrlShiftUpDown(redoTree, JComponent.WHEN_FOCUSED);
116
117 JPanel treesPanel = new JPanel(new GridBagLayout());
118
119 treesPanel.add(spacer, GBC.eol());
120 spacer.setVisible(false);
121 treesPanel.add(undoTree, GBC.eol().fill(GBC.HORIZONTAL));
122 separator.setVisible(false);
123 treesPanel.add(separator, GBC.eol().fill(GBC.HORIZONTAL));
124 treesPanel.add(redoTree, GBC.eol().fill(GBC.HORIZONTAL));
125 treesPanel.add(Box.createRigidArea(new Dimension(0, 0)), GBC.std().weight(0, 1));
126 treesPanel.setBackground(redoTree.getBackground());
127
128 wireUpdateEnabledStateUpdater(selectAction, undoTree);
129 wireUpdateEnabledStateUpdater(selectAction, redoTree);
130
131 UndoRedoAction undoAction = new UndoRedoAction(UndoRedoType.UNDO);
132 wireUpdateEnabledStateUpdater(undoAction, undoTree);
133
134 UndoRedoAction redoAction = new UndoRedoAction(UndoRedoType.REDO);
135 wireUpdateEnabledStateUpdater(redoAction, redoTree);
136
137 scrollPane = (JScrollPane) createLayout(treesPanel, true, Arrays.asList(
138 new SideButton(selectAction),
139 new SideButton(undoAction),
140 new SideButton(redoAction)
141 ));
142
143 InputMapUtils.addEnterAction(undoTree, selectAndZoomAction);
144 InputMapUtils.addEnterAction(redoTree, selectAndZoomAction);
145 }
146
147 private static class CommandCellRenderer extends DefaultTreeCellRenderer {
148 @Override
149 public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row,
150 boolean hasFocus) {
151 super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
152 DefaultMutableTreeNode v = (DefaultMutableTreeNode) value;
153 if (v.getUserObject() instanceof JLabel) {
154 JLabel l = (JLabel) v.getUserObject();
155 setIcon(l.getIcon());
156 setText(l.getText());
157 }
158 return this;
159 }
160 }
161
162 private void updateTitle() {
163 int undo = undoTreeModel.getChildCount(undoTreeModel.getRoot());
164 int redo = redoTreeModel.getChildCount(redoTreeModel.getRoot());
165 if (undo > 0 || redo > 0) {
166 setTitle(tr("Command Stack: Undo: {0} / Redo: {1}", undo, redo));
167 } else {
168 setTitle(tr("Command Stack"));
169 }
170 }
171
172 /**
173 * Selection listener for undo and redo area.
174 * If one is clicked, takes away the selection from the other, so
175 * it behaves as if it was one component.
176 */
177 private class UndoRedoSelectionListener implements TreeSelectionListener {
178 private final JTree source;
179
180 UndoRedoSelectionListener(JTree source) {
181 this.source = source;
182 }
183
184 @Override
185 public void valueChanged(TreeSelectionEvent e) {
186 if (source == undoTree) {
187 redoTree.getSelectionModel().removeTreeSelectionListener(redoSelectionListener);
188 redoTree.clearSelection();
189 redoTree.getSelectionModel().addTreeSelectionListener(redoSelectionListener);
190 }
191 if (source == redoTree) {
192 undoTree.getSelectionModel().removeTreeSelectionListener(undoSelectionListener);
193 undoTree.clearSelection();
194 undoTree.getSelectionModel().addTreeSelectionListener(undoSelectionListener);
195 }
196 }
197 }
198
199 /**
200 * Wires updater for enabled state to the events. Also updates dialog title if needed.
201 * @param updater updater
202 * @param tree tree on which wire updater
203 */
204 protected void wireUpdateEnabledStateUpdater(final IEnabledStateUpdating updater, JTree tree) {
205 addShowNotifyListener(updater);
206
207 tree.addTreeSelectionListener(e -> updater.updateEnabledState());
208
209 tree.getModel().addTreeModelListener(new TreeModelListener() {
210 @Override
211 public void treeNodesChanged(TreeModelEvent e) {
212 updater.updateEnabledState();
213 updateTitle();
214 }
215
216 @Override
217 public void treeNodesInserted(TreeModelEvent e) {
218 treeNodesChanged(e);
219 }
220
221 @Override
222 public void treeNodesRemoved(TreeModelEvent e) {
223 treeNodesChanged(e);
224 }
225
226 @Override
227 public void treeStructureChanged(TreeModelEvent e) {
228 treeNodesChanged(e);
229 }
230 });
231 }
232
233 @Override
234 public void showNotify() {
235 buildTrees();
236 for (IEnabledStateUpdating listener : showNotifyListener) {
237 listener.updateEnabledState();
238 }
239 MainApplication.undoRedo.addCommandQueuePreciseListener(this);
240 }
241
242 /**
243 * Simple listener setup to update the button enabled state when the side dialog shows.
244 */
245 private final transient Set<IEnabledStateUpdating> showNotifyListener = new LinkedHashSet<>();
246
247 private void addShowNotifyListener(IEnabledStateUpdating listener) {
248 showNotifyListener.add(listener);
249 }
250
251 @Override
252 public void hideNotify() {
253 undoRoot = new DefaultMutableTreeNode();
254 redoRoot = new DefaultMutableTreeNode();
255 undoTreeModel.setRoot(undoRoot);
256 redoTreeModel.setRoot(redoRoot);
257 MainApplication.undoRedo.removeCommandQueuePreciseListener(this);
258 }
259
260 /**
261 * Build the trees of undo and redo commands (initially or when
262 * they have changed).
263 */
264 private void buildTrees() {
265 setTitle(tr("Command Stack"));
266 buildUndoTree();
267 buildRedoTree();
268 ensureTreesConsistency();
269 }
270
271 private void buildUndoTree() {
272 List<Command> undoCommands = MainApplication.undoRedo.commands;
273 undoRoot = new DefaultMutableTreeNode();
274 for (int i = 0; i < undoCommands.size(); ++i) {
275 undoRoot.add(getNodeForCommand(undoCommands.get(i)));
276 }
277 undoTreeModel.setRoot(undoRoot);
278 }
279
280 private void buildRedoTree() {
281 List<Command> redoCommands = MainApplication.undoRedo.redoCommands;
282 redoRoot = new DefaultMutableTreeNode();
283 for (int i = 0; i < redoCommands.size(); ++i) {
284 redoRoot.add(getNodeForCommand(redoCommands.get(i)));
285 }
286 redoTreeModel.setRoot(redoRoot);
287 }
288
289 private void ensureTreesConsistency() {
290 List<Command> undoCommands = MainApplication.undoRedo.commands;
291 List<Command> redoCommands = MainApplication.undoRedo.redoCommands;
292 if (redoTreeModel.getChildCount(redoRoot) > 0) {
293 redoTree.scrollRowToVisible(0);
294 scrollPane.getHorizontalScrollBar().setValue(0);
295 }
296
297 separator.setVisible(!undoCommands.isEmpty() || !redoCommands.isEmpty());
298 spacer.setVisible(undoCommands.isEmpty() && !redoCommands.isEmpty());
299
300 // if one tree is empty, move selection to the other
301 switch (lastOperation) {
302 case UNDO:
303 if (undoCommands.isEmpty()) {
304 lastOperation = UndoRedoType.REDO;
305 }
306 break;
307 case REDO:
308 if (redoCommands.isEmpty()) {
309 lastOperation = UndoRedoType.UNDO;
310 }
311 break;
312 }
313
314 // select the next command to undo/redo
315 switch (lastOperation) {
316 case UNDO:
317 undoTree.setSelectionRow(undoTree.getRowCount()-1);
318 break;
319 case REDO:
320 redoTree.setSelectionRow(0);
321 break;
322 }
323
324 undoTree.scrollRowToVisible(undoTreeModel.getChildCount(undoRoot)-1);
325 scrollPane.getHorizontalScrollBar().setValue(0);
326 }
327
328 /**
329 * Wraps a command in a CommandListMutableTreeNode.
330 * Recursively adds child commands.
331 * @param c the command
332 * @return the resulting node
333 */
334 protected CommandListMutableTreeNode getNodeForCommand(PseudoCommand c) {
335 CommandListMutableTreeNode node = new CommandListMutableTreeNode(c);
336 if (c.getChildren() != null) {
337 List<PseudoCommand> children = new ArrayList<>(c.getChildren());
338 for (int i = 0; i < children.size(); ++i) {
339 node.add(getNodeForCommand(children.get(i)));
340 }
341 }
342 return node;
343 }
344
345 /**
346 * Return primitives that are affected by some command
347 * @param path GUI elements
348 * @return collection of affected primitives, onluy usable ones
349 */
350 protected static Collection<? extends OsmPrimitive> getAffectedPrimitives(TreePath path) {
351 PseudoCommand c = ((CommandListMutableTreeNode) path.getLastPathComponent()).getCommand();
352 final OsmDataLayer currentLayer = MainApplication.getLayerManager().getEditLayer();
353 return new SubclassFilteredCollection<>(
354 c.getParticipatingPrimitives(),
355 o -> {
356 OsmPrimitive p = currentLayer.data.getPrimitiveById(o);
357 return p != null && p.isUsable();
358 }
359 );
360 }
361
362 @Override
363 public void cleaned(CommandQueueCleanedEvent e) {
364 if (isVisible()) {
365 buildTrees();
366 }
367 }
368
369 @Override
370 public void commandAdded(CommandAddedEvent e) {
371 if (isVisible()) {
372 undoRoot.add(getNodeForCommand(e.getCommand()));
373 undoTreeModel.nodeStructureChanged(undoRoot);
374 ensureTreesConsistency();
375 }
376 }
377
378 @Override
379 public void commandUndone(CommandUndoneEvent e) {
380 if (isVisible()) {
381 swapNode(undoTreeModel, undoRoot, undoRoot.getChildCount() - 1, redoTreeModel, redoRoot, 0);
382 }
383 }
384
385 @Override
386 public void commandRedone(CommandRedoneEvent e) {
387 if (isVisible()) {
388 swapNode(redoTreeModel, redoRoot, 0, undoTreeModel, undoRoot, undoRoot.getChildCount());
389 }
390 }
391
392 private void swapNode(DefaultTreeModel srcModel, DefaultMutableTreeNode srcRoot, int srcIndex,
393 DefaultTreeModel dstModel, DefaultMutableTreeNode dstRoot, int dstIndex) {
394 MutableTreeNode node = (MutableTreeNode) srcRoot.getChildAt(srcIndex);
395 srcRoot.remove(node);
396 srcModel.nodeStructureChanged(srcRoot);
397 dstRoot.insert(node, dstIndex);
398 dstModel.nodeStructureChanged(dstRoot);
399 ensureTreesConsistency();
400 }
401
402 /**
403 * Action that selects the objects that take part in a command.
404 */
405 public class SelectAction extends AbstractAction implements IEnabledStateUpdating {
406
407 /**
408 * Constructs a new {@code SelectAction}.
409 */
410 public SelectAction() {
411 putValue(NAME, tr("Select"));
412 putValue(SHORT_DESCRIPTION, tr("Selects the objects that take part in this command (unless currently deleted)"));
413 new ImageProvider("dialogs", "select").getResource().attachImageIcon(this, true);
414 }
415
416 @Override
417 public void actionPerformed(ActionEvent e) {
418 TreePath path;
419 if (!undoTree.isSelectionEmpty()) {
420 path = undoTree.getSelectionPath();
421 } else if (!redoTree.isSelectionEmpty()) {
422 path = redoTree.getSelectionPath();
423 } else
424 throw new IllegalStateException();
425
426 DataSet dataSet = MainApplication.getLayerManager().getEditDataSet();
427 if (dataSet == null) return;
428 dataSet.setSelected(getAffectedPrimitives(path));
429 }
430
431 @Override
432 public void updateEnabledState() {
433 setEnabled(!undoTree.isSelectionEmpty() || !redoTree.isSelectionEmpty());
434 }
435 }
436
437 /**
438 * Action that selects the objects that take part in a command, then zoom to them.
439 */
440 public class SelectAndZoomAction extends SelectAction {
441 /**
442 * Constructs a new {@code SelectAndZoomAction}.
443 */
444 public SelectAndZoomAction() {
445 putValue(NAME, tr("Select and zoom"));
446 putValue(SHORT_DESCRIPTION,
447 tr("Selects the objects that take part in this command (unless currently deleted), then and zooms to it"));
448 new ImageProvider("dialogs/autoscale", "selection").getResource().attachImageIcon(this, true);
449 }
450
451 @Override
452 public void actionPerformed(ActionEvent e) {
453 super.actionPerformed(e);
454 AutoScaleAction.autoScale("selection");
455 }
456 }
457
458 /**
459 * undo / redo switch to reduce duplicate code
460 */
461 protected enum UndoRedoType {
462 UNDO,
463 REDO
464 }
465
466 /**
467 * Action to undo or redo all commands up to (and including) the seleced item.
468 */
469 protected class UndoRedoAction extends AbstractAction implements IEnabledStateUpdating {
470 private final UndoRedoType type;
471 private final JTree tree;
472
473 /**
474 * constructor
475 * @param type decide whether it is an undo action or a redo action
476 */
477 public UndoRedoAction(UndoRedoType type) {
478 this.type = type;
479 if (UndoRedoType.UNDO == type) {
480 tree = undoTree;
481 putValue(NAME, tr("Undo"));
482 putValue(SHORT_DESCRIPTION, tr("Undo the selected and all later commands"));
483 new ImageProvider("undo").getResource().attachImageIcon(this, true);
484 } else {
485 tree = redoTree;
486 putValue(NAME, tr("Redo"));
487 putValue(SHORT_DESCRIPTION, tr("Redo the selected and all earlier commands"));
488 new ImageProvider("redo").getResource().attachImageIcon(this, true);
489 }
490 }
491
492 @Override
493 public void actionPerformed(ActionEvent e) {
494 lastOperation = type;
495 TreePath path = tree.getSelectionPath();
496
497 // we can only undo top level commands
498 if (path.getPathCount() != 2)
499 throw new IllegalStateException();
500
501 int idx = ((CommandListMutableTreeNode) path.getLastPathComponent()).getIndex();
502
503 // calculate the number of commands to undo/redo; then do it
504 switch (type) {
505 case UNDO:
506 int numUndo = ((DefaultMutableTreeNode) undoTreeModel.getRoot()).getChildCount() - idx;
507 MainApplication.undoRedo.undo(numUndo);
508 break;
509 case REDO:
510 int numRedo = idx+1;
511 MainApplication.undoRedo.redo(numRedo);
512 break;
513 }
514 MainApplication.getMap().repaint();
515 }
516
517 @Override
518 public void updateEnabledState() {
519 // do not allow execution if nothing is selected or a sub command was selected
520 setEnabled(!tree.isSelectionEmpty() && tree.getSelectionPath().getPathCount() == 2);
521 }
522 }
523
524 class MouseEventHandler extends PopupMenuLauncher {
525
526 MouseEventHandler() {
527 super(new CommandStackPopup());
528 }
529
530 @Override
531 public void mouseClicked(MouseEvent evt) {
532 if (isDoubleClick(evt)) {
533 selectAndZoomAction.actionPerformed(null);
534 }
535 }
536 }
537
538 private class CommandStackPopup extends JPopupMenu {
539 CommandStackPopup() {
540 add(selectAction);
541 add(selectAndZoomAction);
542 }
543 }
544}
Note: See TracBrowser for help on using the repository browser.