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