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

Last change on this file since 12718 was 12718, checked in by Don-vip, 7 years ago

see #13036 - see #15229 - see #15182 - make Commands depends only on a DataSet, not a Layer. This removes a lot of GUI dependencies

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