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 053public class CommandStackDialog extends ToggleDialog implements CommandQueueListener { 054 055 private DefaultTreeModel undoTreeModel = new DefaultTreeModel(new DefaultMutableTreeNode()); 056 private DefaultTreeModel redoTreeModel = new DefaultTreeModel(new DefaultMutableTreeNode()); 057 058 private JTree undoTree = new JTree(undoTreeModel); 059 private JTree redoTree = new JTree(redoTreeModel); 060 061 private UndoRedoSelectionListener undoSelectionListener; 062 private UndoRedoSelectionListener redoSelectionListener; 063 064 private JScrollPane scrollPane; 065 private JSeparator separator = new JSeparator(); 066 // only visible, if separator is the top most component 067 private Component spacer = Box.createRigidArea(new Dimension(0, 3)); 068 069 // last operation is remembered to select the next undo/redo entry in the list 070 // after undo/redo command 071 private UndoRedoType lastOperation = UndoRedoType.UNDO; 072 073 // Actions for context menu and Enter key 074 private SelectAction selectAction = new SelectAction(); 075 private SelectAndZoomAction selectAndZoomAction = new SelectAndZoomAction(); 076 077 /** 078 * Constructs a new {@code CommandStackDialog}. 079 */ 080 public CommandStackDialog() { 081 super(tr("Command Stack"), "commandstack", tr("Open a list of all commands (undo buffer)."), 082 Shortcut.registerShortcut("subwindow:commandstack", tr("Toggle: {0}", 083 tr("Command Stack")), KeyEvent.VK_O, Shortcut.ALT_SHIFT), 100); 084 undoTree.addMouseListener(new MouseEventHandler()); 085 undoTree.setRootVisible(false); 086 undoTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); 087 undoTree.setShowsRootHandles(true); 088 undoTree.expandRow(0); 089 undoTree.setCellRenderer(new CommandCellRenderer()); 090 undoSelectionListener = new UndoRedoSelectionListener(undoTree); 091 undoTree.getSelectionModel().addTreeSelectionListener(undoSelectionListener); 092 InputMapUtils.unassignCtrlShiftUpDown(undoTree, JComponent.WHEN_FOCUSED); 093 094 redoTree.addMouseListener(new MouseEventHandler()); 095 redoTree.setRootVisible(false); 096 redoTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); 097 redoTree.setShowsRootHandles(true); 098 redoTree.expandRow(0); 099 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 @Override 159 public void valueChanged(TreeSelectionEvent e) { 160 if (source == undoTree) { 161 redoTree.getSelectionModel().removeTreeSelectionListener(redoSelectionListener); 162 redoTree.clearSelection(); 163 redoTree.getSelectionModel().addTreeSelectionListener(redoSelectionListener); 164 } 165 if (source == redoTree) { 166 undoTree.getSelectionModel().removeTreeSelectionListener(undoSelectionListener); 167 undoTree.clearSelection(); 168 undoTree.getSelectionModel().addTreeSelectionListener(undoSelectionListener); 169 } 170 } 171 } 172 173 /** 174 * Interface to provide a callback for enabled state update. 175 */ 176 protected interface IEnabledStateUpdating { 177 void updateEnabledState(); 178 } 179 180 /** 181 * Wires updater for enabled state to the events. 182 */ 183 protected void wireUpdateEnabledStateUpdater(final IEnabledStateUpdating updater, JTree tree) { 184 addShowNotifyListener(updater); 185 186 tree.addTreeSelectionListener(new TreeSelectionListener() { 187 @Override 188 public void valueChanged(TreeSelectionEvent e) { 189 updater.updateEnabledState(); 190 } 191 }); 192 193 tree.getModel().addTreeModelListener(new TreeModelListener() { 194 @Override 195 public void treeNodesChanged(TreeModelEvent e) { 196 updater.updateEnabledState(); 197 } 198 199 @Override 200 public void treeNodesInserted(TreeModelEvent e) { 201 updater.updateEnabledState(); 202 } 203 204 @Override 205 public void treeNodesRemoved(TreeModelEvent e) { 206 updater.updateEnabledState(); 207 } 208 209 @Override 210 public void treeStructureChanged(TreeModelEvent e) { 211 updater.updateEnabledState(); 212 } 213 }); 214 } 215 216 @Override 217 public void showNotify() { 218 buildTrees(); 219 for (IEnabledStateUpdating listener : showNotifyListener) { 220 listener.updateEnabledState(); 221 } 222 Main.main.undoRedo.addCommandQueueListener(this); 223 } 224 225 /** 226 * Simple listener setup to update the button enabled state when the side dialog shows. 227 */ 228 Set<IEnabledStateUpdating> showNotifyListener = new LinkedHashSet<>(); 229 230 private void addShowNotifyListener(IEnabledStateUpdating listener) { 231 showNotifyListener.add(listener); 232 } 233 234 @Override 235 public void hideNotify() { 236 undoTreeModel.setRoot(new DefaultMutableTreeNode()); 237 redoTreeModel.setRoot(new DefaultMutableTreeNode()); 238 Main.main.undoRedo.removeCommandQueueListener(this); 239 } 240 241 /** 242 * Build the trees of undo and redo commands (initially or when 243 * they have changed). 244 */ 245 private void buildTrees() { 246 setTitle(tr("Command Stack")); 247 if (!Main.main.hasEditLayer()) 248 return; 249 250 List<Command> undoCommands = Main.main.undoRedo.commands; 251 DefaultMutableTreeNode undoRoot = new DefaultMutableTreeNode(); 252 for (int i=0; i<undoCommands.size(); ++i) { 253 undoRoot.add(getNodeForCommand(undoCommands.get(i), i)); 254 } 255 undoTreeModel.setRoot(undoRoot); 256 257 List<Command> redoCommands = Main.main.undoRedo.redoCommands; 258 DefaultMutableTreeNode redoRoot = new DefaultMutableTreeNode(); 259 for (int i=0; i<redoCommands.size(); ++i) { 260 redoRoot.add(getNodeForCommand(redoCommands.get(i), i)); 261 } 262 redoTreeModel.setRoot(redoRoot); 263 if (redoTreeModel.getChildCount(redoRoot) > 0) { 264 redoTree.scrollRowToVisible(0); 265 scrollPane.getHorizontalScrollBar().setValue(0); 266 } 267 268 separator.setVisible(!undoCommands.isEmpty() || !redoCommands.isEmpty()); 269 spacer.setVisible(undoCommands.isEmpty() && !redoCommands.isEmpty()); 270 271 // if one tree is empty, move selection to the other 272 switch (lastOperation) { 273 case UNDO: 274 if (undoCommands.isEmpty()) { 275 lastOperation = UndoRedoType.REDO; 276 } 277 break; 278 case REDO: 279 if (redoCommands.isEmpty()) { 280 lastOperation = UndoRedoType.UNDO; 281 } 282 break; 283 } 284 285 // select the next command to undo/redo 286 switch (lastOperation) { 287 case UNDO: 288 undoTree.setSelectionRow(undoTree.getRowCount()-1); 289 break; 290 case REDO: 291 redoTree.setSelectionRow(0); 292 break; 293 } 294 295 undoTree.scrollRowToVisible(undoTreeModel.getChildCount(undoRoot)-1); 296 scrollPane.getHorizontalScrollBar().setValue(0); 297 } 298 299 /** 300 * Wraps a command in a CommandListMutableTreeNode. 301 * Recursively adds child commands. 302 */ 303 protected CommandListMutableTreeNode getNodeForCommand(PseudoCommand c, int idx) { 304 CommandListMutableTreeNode node = new CommandListMutableTreeNode(c, idx); 305 if (c.getChildren() != null) { 306 List<PseudoCommand> children = new ArrayList<>(c.getChildren()); 307 for (int i=0; i<children.size(); ++i) { 308 node.add(getNodeForCommand(children.get(i), i)); 309 } 310 } 311 return node; 312 } 313 314 /** 315 * Return primitives that are affected by some command 316 * @param path GUI elements 317 * @return collection of affected primitives, onluy usable ones 318 */ 319 protected static FilteredCollection<OsmPrimitive> getAffectedPrimitives(TreePath path) { 320 PseudoCommand c = ((CommandListMutableTreeNode) path.getLastPathComponent()).getCommand(); 321 final OsmDataLayer currentLayer = Main.main.getEditLayer(); 322 return new FilteredCollection<>( 323 c.getParticipatingPrimitives(), 324 new Predicate<OsmPrimitive>(){ 325 @Override 326 public boolean evaluate(OsmPrimitive o) { 327 OsmPrimitive p = currentLayer.data.getPrimitiveById(o); 328 return p != null && p.isUsable(); 329 } 330 } 331 ); 332 } 333 334 @Override 335 public void commandChanged(int queueSize, int redoSize) { 336 if (!isVisible()) 337 return; 338 buildTrees(); 339 } 340 341 public class SelectAction extends AbstractAction implements IEnabledStateUpdating { 342 343 /** 344 * Constructs a new {@code SelectAction}. 345 */ 346 public SelectAction() { 347 putValue(NAME,tr("Select")); 348 putValue(SHORT_DESCRIPTION, tr("Selects the objects that take part in this command (unless currently deleted)")); 349 putValue(SMALL_ICON, ImageProvider.get("dialogs","select")); 350 } 351 352 @Override 353 public void actionPerformed(ActionEvent e) { 354 TreePath path; 355 undoTree.getSelectionPath(); 356 if (!undoTree.isSelectionEmpty()) { 357 path = undoTree.getSelectionPath(); 358 } else if (!redoTree.isSelectionEmpty()) { 359 path = redoTree.getSelectionPath(); 360 } else 361 throw new IllegalStateException(); 362 363 OsmDataLayer editLayer = Main.main.getEditLayer(); 364 if (editLayer == null) return; 365 editLayer.data.setSelected( getAffectedPrimitives(path)); 366 } 367 368 @Override 369 public void updateEnabledState() { 370 setEnabled(!undoTree.isSelectionEmpty() || !redoTree.isSelectionEmpty()); 371 } 372 } 373 374 public class SelectAndZoomAction extends SelectAction { 375 /** 376 * Constructs a new {@code SelectAndZoomAction}. 377 */ 378 public SelectAndZoomAction() { 379 putValue(NAME,tr("Select and zoom")); 380 putValue(SHORT_DESCRIPTION, tr("Selects the objects that take part in this command (unless currently deleted), then and zooms to it")); 381 putValue(SMALL_ICON, ImageProvider.get("dialogs/autoscale","selection")); 382 } 383 384 @Override 385 public void actionPerformed(ActionEvent e) { 386 super.actionPerformed(e); 387 if (!Main.main.hasEditLayer()) return; 388 AutoScaleAction.autoScale("selection"); 389 } 390 } 391 392 /** 393 * undo / redo switch to reduce duplicate code 394 */ 395 protected enum UndoRedoType {UNDO, REDO} 396 397 /** 398 * Action to undo or redo all commands up to (and including) the seleced item. 399 */ 400 protected class UndoRedoAction extends AbstractAction implements IEnabledStateUpdating { 401 private UndoRedoType type; 402 private JTree tree; 403 404 /** 405 * constructor 406 * @param type decide whether it is an undo action or a redo action 407 */ 408 public UndoRedoAction(UndoRedoType type) { 409 super(); 410 this.type = type; 411 switch (type) { 412 case UNDO: 413 tree = undoTree; 414 putValue(NAME,tr("Undo")); 415 putValue(SHORT_DESCRIPTION, tr("Undo the selected and all later commands")); 416 putValue(SMALL_ICON, ImageProvider.get("undo")); 417 break; 418 case REDO: 419 tree = redoTree; 420 putValue(NAME,tr("Redo")); 421 putValue(SHORT_DESCRIPTION, tr("Redo the selected and all earlier commands")); 422 putValue(SMALL_ICON, ImageProvider.get("redo")); 423 break; 424 } 425 } 426 427 @Override 428 public void actionPerformed(ActionEvent e) { 429 lastOperation = type; 430 TreePath path = tree.getSelectionPath(); 431 432 // we can only undo top level commands 433 if (path.getPathCount() != 2) 434 throw new IllegalStateException(); 435 436 int idx = ((CommandListMutableTreeNode) path.getLastPathComponent()).getIndex(); 437 438 // calculate the number of commands to undo/redo; then do it 439 switch (type) { 440 case UNDO: 441 int numUndo = ((DefaultMutableTreeNode) undoTreeModel.getRoot()).getChildCount() - idx; 442 Main.main.undoRedo.undo(numUndo); 443 break; 444 case REDO: 445 int numRedo = idx+1; 446 Main.main.undoRedo.redo(numRedo); 447 break; 448 } 449 Main.map.repaint(); 450 } 451 452 @Override 453 public void updateEnabledState() { 454 // do not allow execution if nothing is selected or a sub command was selected 455 setEnabled(!tree.isSelectionEmpty() && tree.getSelectionPath().getPathCount()==2); 456 } 457 } 458 459 class MouseEventHandler extends PopupMenuLauncher { 460 461 public MouseEventHandler() { 462 super(new CommandStackPopup()); 463 } 464 465 @Override 466 public void mouseClicked(MouseEvent evt) { 467 if (isDoubleClick(evt)) { 468 selectAndZoomAction.actionPerformed(null); 469 } 470 } 471 } 472 473 private class CommandStackPopup extends JPopupMenu { 474 public CommandStackPopup(){ 475 add(selectAction); 476 add(selectAndZoomAction); 477 } 478 } 479}