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

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

see #15229 - deprecate Main*.undoRedo - make UndoRedoHandler a singleton

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