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

Last change on this file since 10356 was 10356, checked in by stoecker, 8 years ago

see #9995, see #10684 - remove more hardcoded places of images

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