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

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

fix #13023 - Replace uses of hasEditLayer() with new layer manager (patch by michael2402, modified) - gsoc-core

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