001 // License: GPL. Copyright 2007 by Immanuel Scholz and others 002 package org.openstreetmap.josm.gui.dialogs; 003 004 import static org.openstreetmap.josm.tools.I18n.tr; 005 006 import java.awt.BorderLayout; 007 import java.awt.Component; 008 import java.awt.Dimension; 009 import java.awt.GridBagLayout; 010 import java.awt.Point; 011 import java.awt.event.ActionEvent; 012 import java.awt.event.KeyEvent; 013 import java.awt.event.MouseEvent; 014 import java.util.ArrayList; 015 import java.util.Arrays; 016 import java.util.LinkedHashSet; 017 import java.util.List; 018 import java.util.Set; 019 020 import javax.swing.AbstractAction; 021 import javax.swing.Box; 022 import javax.swing.JComponent; 023 import javax.swing.JLabel; 024 import javax.swing.JPanel; 025 import javax.swing.JPopupMenu; 026 import javax.swing.JScrollPane; 027 import javax.swing.JSeparator; 028 import javax.swing.JTree; 029 import javax.swing.event.TreeModelEvent; 030 import javax.swing.event.TreeModelListener; 031 import javax.swing.event.TreeSelectionEvent; 032 import javax.swing.event.TreeSelectionListener; 033 import javax.swing.tree.DefaultMutableTreeNode; 034 import javax.swing.tree.DefaultTreeCellRenderer; 035 import javax.swing.tree.DefaultTreeModel; 036 import javax.swing.tree.TreePath; 037 import javax.swing.tree.TreeSelectionModel; 038 039 import org.openstreetmap.josm.Main; 040 import org.openstreetmap.josm.command.Command; 041 import org.openstreetmap.josm.command.PseudoCommand; 042 import org.openstreetmap.josm.data.osm.OsmPrimitive; 043 import org.openstreetmap.josm.gui.MapFrame; 044 import org.openstreetmap.josm.gui.SideButton; 045 import org.openstreetmap.josm.gui.layer.OsmDataLayer; 046 import org.openstreetmap.josm.gui.layer.OsmDataLayer.CommandQueueListener; 047 import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher; 048 import org.openstreetmap.josm.tools.FilteredCollection; 049 import org.openstreetmap.josm.tools.GBC; 050 import org.openstreetmap.josm.tools.ImageProvider; 051 import org.openstreetmap.josm.tools.InputMapUtils; 052 import org.openstreetmap.josm.tools.Predicate; 053 import org.openstreetmap.josm.tools.Shortcut; 054 055 public class CommandStackDialog extends ToggleDialog implements CommandQueueListener { 056 057 private DefaultTreeModel undoTreeModel = new DefaultTreeModel(new DefaultMutableTreeNode()); 058 private DefaultTreeModel redoTreeModel = new DefaultTreeModel(new DefaultMutableTreeNode()); 059 060 private JTree undoTree = new JTree(undoTreeModel); 061 private JTree redoTree = new JTree(redoTreeModel); 062 063 private UndoRedoSelectionListener undoSelectionListener; 064 private UndoRedoSelectionListener redoSelectionListener; 065 066 private JScrollPane scrollPane; 067 private JSeparator separator = new JSeparator(); 068 // only visible, if separator is the top most component 069 private Component spacer = Box.createRigidArea(new Dimension(0, 3)); 070 071 // last operation is remembered to select the next undo/redo entry in the list 072 // after undo/redo command 073 private UndoRedoType lastOperation = UndoRedoType.UNDO; 074 075 public CommandStackDialog(final MapFrame mapFrame) { 076 super(tr("Command Stack"), "commandstack", tr("Open a list of all commands (undo buffer)."), 077 Shortcut.registerShortcut("subwindow:commandstack", tr("Toggle: {0}", 078 tr("Command Stack")), KeyEvent.VK_O, Shortcut.ALT_SHIFT), 100, true); 079 undoTree.addMouseListener(new PopupMenuHandler()); 080 undoTree.setRootVisible(false); 081 undoTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); 082 undoTree.setShowsRootHandles(true); 083 undoTree.expandRow(0); 084 undoTree.setCellRenderer(new CommandCellRenderer()); 085 undoSelectionListener = new UndoRedoSelectionListener(undoTree); 086 undoTree.getSelectionModel().addTreeSelectionListener(undoSelectionListener); 087 InputMapUtils.unassignCtrlShiftUpDown(undoTree, JComponent.WHEN_FOCUSED); 088 089 redoTree.addMouseListener(new PopupMenuHandler()); 090 redoTree.setRootVisible(false); 091 redoTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); 092 redoTree.setShowsRootHandles(true); 093 redoTree.expandRow(0); 094 redoTree.setCellRenderer(new CommandCellRenderer()); 095 redoSelectionListener = new UndoRedoSelectionListener(redoTree); 096 redoTree.getSelectionModel().addTreeSelectionListener(redoSelectionListener); 097 098 JPanel treesPanel = new JPanel(new GridBagLayout()); 099 100 treesPanel.add(spacer, GBC.eol()); 101 spacer.setVisible(false); 102 treesPanel.add(undoTree, GBC.eol().fill(GBC.HORIZONTAL)); 103 separator.setVisible(false); 104 treesPanel.add(separator, GBC.eol().fill(GBC.HORIZONTAL)); 105 treesPanel.add(redoTree, GBC.eol().fill(GBC.HORIZONTAL)); 106 treesPanel.add(Box.createRigidArea(new Dimension(0, 0)), GBC.std().weight(0, 1)); 107 treesPanel.setBackground(redoTree.getBackground()); 108 109 SelectAction selectAction = new SelectAction(); 110 wireUpdateEnabledStateUpdater(selectAction, undoTree); 111 wireUpdateEnabledStateUpdater(selectAction, redoTree); 112 113 UndoRedoAction undoAction = new UndoRedoAction(UndoRedoType.UNDO); 114 wireUpdateEnabledStateUpdater(undoAction, undoTree); 115 116 UndoRedoAction redoAction = new UndoRedoAction(UndoRedoType.REDO); 117 wireUpdateEnabledStateUpdater(redoAction, redoTree); 118 119 scrollPane = (JScrollPane)createLayout(treesPanel, true, Arrays.asList(new SideButton[] { 120 new SideButton(selectAction), 121 new SideButton(undoAction), 122 new SideButton(redoAction) 123 })); 124 125 InputMapUtils.addEnterAction(undoTree, selectAction); 126 InputMapUtils.addEnterAction(redoTree, selectAction); 127 } 128 129 private static class CommandCellRenderer extends DefaultTreeCellRenderer { 130 @Override public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) { 131 super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus); 132 DefaultMutableTreeNode v = (DefaultMutableTreeNode)value; 133 if (v.getUserObject() instanceof JLabel) { 134 JLabel l = (JLabel)v.getUserObject(); 135 setIcon(l.getIcon()); 136 setText(l.getText()); 137 } 138 return this; 139 } 140 } 141 142 /** 143 * Selection listener for undo and redo area. 144 * If one is clicked, takes away the selection from the other, so 145 * it behaves as if it was one component. 146 */ 147 private class UndoRedoSelectionListener implements TreeSelectionListener { 148 private JTree source; 149 150 public UndoRedoSelectionListener(JTree source) { 151 this.source = source; 152 } 153 154 public void valueChanged(TreeSelectionEvent e) { 155 if (source == undoTree) { 156 redoTree.getSelectionModel().removeTreeSelectionListener(redoSelectionListener); 157 redoTree.clearSelection(); 158 redoTree.getSelectionModel().addTreeSelectionListener(redoSelectionListener); 159 } 160 if (source == redoTree) { 161 undoTree.getSelectionModel().removeTreeSelectionListener(undoSelectionListener); 162 undoTree.clearSelection(); 163 undoTree.getSelectionModel().addTreeSelectionListener(undoSelectionListener); 164 } 165 } 166 } 167 168 /** 169 * Interface to provide a callback for enabled state update. 170 */ 171 protected interface IEnabledStateUpdating { 172 void updateEnabledState(); 173 } 174 175 /** 176 * Wires updater for enabled state to the events. 177 */ 178 protected void wireUpdateEnabledStateUpdater(final IEnabledStateUpdating updater, JTree tree) { 179 addShowNotifyListener(updater); 180 181 tree.addTreeSelectionListener(new TreeSelectionListener() { 182 public void valueChanged(TreeSelectionEvent e) { 183 updater.updateEnabledState(); 184 } 185 }); 186 187 tree.getModel().addTreeModelListener(new TreeModelListener() { 188 public void treeNodesChanged(TreeModelEvent e) { 189 updater.updateEnabledState(); 190 } 191 192 public void treeNodesInserted(TreeModelEvent e) { 193 updater.updateEnabledState(); 194 } 195 196 public void treeNodesRemoved(TreeModelEvent e) { 197 updater.updateEnabledState(); 198 } 199 200 public void treeStructureChanged(TreeModelEvent e) { 201 updater.updateEnabledState(); 202 } 203 }); 204 } 205 206 @Override 207 public void showNotify() { 208 buildTrees(); 209 for (IEnabledStateUpdating listener : showNotifyListener) { 210 listener.updateEnabledState(); 211 } 212 Main.main.undoRedo.addCommandQueueListener(this); 213 } 214 215 /** 216 * Simple listener setup to update the button enabled state when the side dialog shows. 217 */ 218 Set<IEnabledStateUpdating> showNotifyListener = new LinkedHashSet<IEnabledStateUpdating>(); 219 220 private void addShowNotifyListener(IEnabledStateUpdating listener) { 221 showNotifyListener.add(listener); 222 } 223 224 @Override 225 public void hideNotify() { 226 undoTreeModel.setRoot(new DefaultMutableTreeNode()); 227 redoTreeModel.setRoot(new DefaultMutableTreeNode()); 228 Main.main.undoRedo.removeCommandQueueListener(this); 229 } 230 231 /** 232 * Build the trees of undo and redo commands (initially or when 233 * they have changed). 234 */ 235 private void buildTrees() { 236 setTitle(tr("Command Stack")); 237 if (Main.map == null || Main.map.mapView == null || Main.map.mapView.getEditLayer() == null) 238 return; 239 240 List<Command> undoCommands = Main.main.undoRedo.commands; 241 DefaultMutableTreeNode undoRoot = new DefaultMutableTreeNode(); 242 for (int i=0; i<undoCommands.size(); ++i) { 243 undoRoot.add(getNodeForCommand(undoCommands.get(i), i)); 244 } 245 undoTreeModel.setRoot(undoRoot); 246 247 List<Command> redoCommands = Main.main.undoRedo.redoCommands; 248 DefaultMutableTreeNode redoRoot = new DefaultMutableTreeNode(); 249 for (int i=0; i<redoCommands.size(); ++i) { 250 redoRoot.add(getNodeForCommand(redoCommands.get(i), i)); 251 } 252 redoTreeModel.setRoot(redoRoot); 253 if (redoTreeModel.getChildCount(redoRoot) > 0) { 254 redoTree.scrollRowToVisible(0); 255 scrollPane.getHorizontalScrollBar().setValue(0); 256 } 257 258 separator.setVisible(!undoCommands.isEmpty() || !redoCommands.isEmpty()); 259 spacer.setVisible(undoCommands.isEmpty() && !redoCommands.isEmpty()); 260 261 // if one tree is empty, move selection to the other 262 switch (lastOperation) { 263 case UNDO: 264 if (undoCommands.isEmpty()) { 265 lastOperation = UndoRedoType.REDO; 266 } 267 break; 268 case REDO: 269 if (redoCommands.isEmpty()) { 270 lastOperation = UndoRedoType.UNDO; 271 } 272 break; 273 } 274 275 // select the next command to undo/redo 276 switch (lastOperation) { 277 case UNDO: 278 undoTree.setSelectionRow(undoTree.getRowCount()-1); 279 break; 280 case REDO: 281 redoTree.setSelectionRow(0); 282 break; 283 } 284 285 undoTree.scrollRowToVisible(undoTreeModel.getChildCount(undoRoot)-1); 286 scrollPane.getHorizontalScrollBar().setValue(0); 287 } 288 289 /** 290 * Wraps a command in a CommandListMutableTreeNode. 291 * Recursively adds child commands. 292 */ 293 protected CommandListMutableTreeNode getNodeForCommand(PseudoCommand c, int idx) { 294 CommandListMutableTreeNode node = new CommandListMutableTreeNode(c, idx); 295 if (c.getChildren() != null) { 296 List<PseudoCommand> children = new ArrayList<PseudoCommand>(c.getChildren()); 297 for (int i=0; i<children.size(); ++i) { 298 node.add(getNodeForCommand(children.get(i), i)); 299 } 300 } 301 return node; 302 } 303 304 public void commandChanged(int queueSize, int redoSize) { 305 if (!isVisible()) 306 return; 307 buildTrees(); 308 } 309 310 public class SelectAction extends AbstractAction implements IEnabledStateUpdating { 311 312 public SelectAction() { 313 super(); 314 putValue(NAME,tr("Select")); 315 putValue(SHORT_DESCRIPTION, tr("Selects the objects that take part in this command (unless currently deleted)")); 316 putValue(SMALL_ICON, ImageProvider.get("dialogs","select")); 317 318 } 319 320 public void actionPerformed(ActionEvent e) { 321 TreePath path; 322 undoTree.getSelectionPath(); 323 if (!undoTree.isSelectionEmpty()) { 324 path = undoTree.getSelectionPath(); 325 } else if (!redoTree.isSelectionEmpty()) { 326 path = redoTree.getSelectionPath(); 327 } else 328 throw new IllegalStateException(); 329 330 if (Main.map == null || Main.map.mapView == null || Main.map.mapView.getEditLayer() == null) return; 331 PseudoCommand c = ((CommandListMutableTreeNode) path.getLastPathComponent()).getCommand(); 332 333 final OsmDataLayer currentLayer = Main.map.mapView.getEditLayer(); 334 335 FilteredCollection<OsmPrimitive> prims = new FilteredCollection<OsmPrimitive>( 336 c.getParticipatingPrimitives(), 337 new Predicate<OsmPrimitive>(){ 338 public boolean evaluate(OsmPrimitive o) { 339 OsmPrimitive p = currentLayer.data.getPrimitiveById(o); 340 return p != null && p.isUsable(); 341 } 342 } 343 ); 344 Main.map.mapView.getEditLayer().data.setSelected(prims); 345 } 346 347 public void updateEnabledState() { 348 setEnabled(!undoTree.isSelectionEmpty() || !redoTree.isSelectionEmpty()); 349 } 350 351 } 352 353 /** 354 * undo / redo switch to reduce duplicate code 355 */ 356 protected enum UndoRedoType {UNDO, REDO}; 357 358 /** 359 * Action to undo or redo all commands up to (and including) the seleced item. 360 */ 361 protected class UndoRedoAction extends AbstractAction implements IEnabledStateUpdating { 362 private UndoRedoType type; 363 private JTree tree; 364 365 /** 366 * constructor 367 * @param type decide whether it is an undo action or a redo action 368 */ 369 public UndoRedoAction(UndoRedoType type) { 370 super(); 371 this.type = type; 372 switch (type) { 373 case UNDO: 374 tree = undoTree; 375 putValue(NAME,tr("Undo")); 376 putValue(SHORT_DESCRIPTION, tr("Undo the selected and all later commands")); 377 putValue(SMALL_ICON, ImageProvider.get("undo")); 378 break; 379 case REDO: 380 tree = redoTree; 381 putValue(NAME,tr("Redo")); 382 putValue(SHORT_DESCRIPTION, tr("Redo the selected and all earlier commands")); 383 putValue(SMALL_ICON, ImageProvider.get("redo")); 384 break; 385 } 386 } 387 388 public void actionPerformed(ActionEvent e) { 389 lastOperation = type; 390 TreePath path = tree.getSelectionPath(); 391 392 // we can only undo top level commands 393 if (path.getPathCount() != 2) 394 throw new IllegalStateException(); 395 396 int idx = ((CommandListMutableTreeNode) path.getLastPathComponent()).getIndex(); 397 398 // calculate the number of commands to undo/redo; then do it 399 switch (type) { 400 case UNDO: 401 int numUndo = ((DefaultMutableTreeNode) undoTreeModel.getRoot()).getChildCount() - idx; 402 Main.main.undoRedo.undo(numUndo); 403 break; 404 case REDO: 405 int numRedo = idx+1; 406 Main.main.undoRedo.redo(numRedo); 407 break; 408 } 409 Main.map.repaint(); 410 } 411 412 public void updateEnabledState() { 413 // do not allow execution if nothing is selected or a sub command was selected 414 setEnabled(!tree.isSelectionEmpty() && tree.getSelectionPath().getPathCount()==2); 415 } 416 } 417 418 class PopupMenuHandler extends PopupMenuLauncher { 419 @Override 420 public void launch(MouseEvent evt) { 421 Point p = evt.getPoint(); 422 JTree tree = (JTree) evt.getSource(); 423 int row = tree.getRowForLocation(p.x, p.y); 424 if (row != -1) { 425 TreePath path = tree.getPathForLocation(p.x, p.y); 426 // right click on unselected element -> select it first 427 if (!tree.isPathSelected(path)) { 428 tree.setSelectionPath(path); 429 } 430 TreePath[] selPaths = tree.getSelectionPaths(); 431 432 CommandStackPopup menu = new CommandStackPopup(selPaths); 433 menu.show(tree, p.x, p.y-3); 434 } 435 } 436 } 437 438 private class CommandStackPopup extends JPopupMenu { 439 private TreePath[] sel; 440 public CommandStackPopup(TreePath[] sel){ 441 this.sel = sel; 442 add(new SelectAction()); 443 } 444 } 445 }