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

Last change on this file since 5200 was 5200, checked in by akks, 12 years ago

see #7626, fix #7463: keys Ctrl-Shift-Up/Down, Enter, Spacebar work better in toggle dialogs
Enter and Spacebar = useful actions for list items (select, toggle, etc.)

  • Property svn:eol-style set to native
File size: 16.8 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.BorderLayout;
7import java.awt.Component;
8import java.awt.Dimension;
9import java.awt.GridBagLayout;
10import java.awt.Point;
11import java.awt.event.ActionEvent;
12import java.awt.event.KeyEvent;
13import java.awt.event.MouseEvent;
14import java.util.ArrayList;
15import java.util.Arrays;
16import java.util.LinkedHashSet;
17import java.util.List;
18import java.util.Set;
19
20import javax.swing.AbstractAction;
21import javax.swing.Box;
22import javax.swing.JComponent;
23import javax.swing.JLabel;
24import javax.swing.JPanel;
25import javax.swing.JPopupMenu;
26import javax.swing.JScrollPane;
27import javax.swing.JSeparator;
28import javax.swing.JTree;
29import javax.swing.event.TreeModelEvent;
30import javax.swing.event.TreeModelListener;
31import javax.swing.event.TreeSelectionEvent;
32import javax.swing.event.TreeSelectionListener;
33import javax.swing.tree.DefaultMutableTreeNode;
34import javax.swing.tree.DefaultTreeCellRenderer;
35import javax.swing.tree.DefaultTreeModel;
36import javax.swing.tree.TreePath;
37import javax.swing.tree.TreeSelectionModel;
38
39import org.openstreetmap.josm.Main;
40import org.openstreetmap.josm.command.Command;
41import org.openstreetmap.josm.command.PseudoCommand;
42import org.openstreetmap.josm.data.osm.OsmPrimitive;
43import org.openstreetmap.josm.gui.MapFrame;
44import org.openstreetmap.josm.gui.SideButton;
45import org.openstreetmap.josm.gui.layer.OsmDataLayer;
46import org.openstreetmap.josm.gui.layer.OsmDataLayer.CommandQueueListener;
47import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
48import org.openstreetmap.josm.tools.FilteredCollection;
49import org.openstreetmap.josm.tools.GBC;
50import org.openstreetmap.josm.tools.ImageProvider;
51import org.openstreetmap.josm.tools.InputMapUtils;
52import org.openstreetmap.josm.tools.Predicate;
53import org.openstreetmap.josm.tools.Shortcut;
54
55public class CommandStackDialog extends ToggleDialog implements CommandQueueListener {
56
57 private DefaultTreeModel undoTreeModel = new DefaultTreeModel(new DefaultMutableTreeNode());
58 private DefaultTreeModel redoTreeModel = new DefaultTreeModel(new DefaultMutableTreeNode());
59
60 private JTree undoTree = new JTree(undoTreeModel);
61 private JTree redoTree = new JTree(redoTreeModel);
62
63 private UndoRedoSelectionListener undoSelectionListener;
64 private UndoRedoSelectionListener redoSelectionListener;
65
66 private JScrollPane scrollPane;
67 private JSeparator separator = new JSeparator();
68 // only visible, if separator is the top most component
69 private Component spacer = Box.createRigidArea(new Dimension(0, 3));
70
71 // last operation is remembered to select the next undo/redo entry in the list
72 // after undo/redo command
73 private UndoRedoType lastOperation = UndoRedoType.UNDO;
74
75 public CommandStackDialog(final MapFrame mapFrame) {
76 super(tr("Command Stack"), "commandstack", tr("Open a list of all commands (undo buffer)."),
77 Shortcut.registerShortcut("subwindow:commandstack", tr("Toggle: {0}",
78 tr("Command Stack")), KeyEvent.VK_O, Shortcut.ALT_SHIFT), 100, true);
79 undoTree.addMouseListener(new PopupMenuHandler());
80 undoTree.setRootVisible(false);
81 undoTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
82 undoTree.setShowsRootHandles(true);
83 undoTree.expandRow(0);
84 undoTree.setCellRenderer(new CommandCellRenderer());
85 undoSelectionListener = new UndoRedoSelectionListener(undoTree);
86 undoTree.getSelectionModel().addTreeSelectionListener(undoSelectionListener);
87 InputMapUtils.unassignCtrlShiftUpDown(undoTree, JComponent.WHEN_FOCUSED);
88
89 redoTree.addMouseListener(new PopupMenuHandler());
90 redoTree.setRootVisible(false);
91 redoTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
92 redoTree.setShowsRootHandles(true);
93 redoTree.expandRow(0);
94 redoTree.setCellRenderer(new CommandCellRenderer());
95 redoSelectionListener = new UndoRedoSelectionListener(redoTree);
96 redoTree.getSelectionModel().addTreeSelectionListener(redoSelectionListener);
97
98 JPanel treesPanel = new JPanel(new GridBagLayout());
99
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}
Note: See TracBrowser for help on using the repository browser.