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

Last change on this file since 13943 was 13943, checked in by Don-vip, 6 months ago

fix #16389 - fix wrong undo/redo behaviour in command stack dialog (regression from r13729)

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