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

Last change on this file since 5727 was 5727, checked in by akks, 11 years ago

Command Stack dialog (undo/redo): select and zoom to affected primitives on double-click, Enter or context menu action

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