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

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

code cleanup / robustness in edit layer handling

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