001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.dialogs;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Component;
007import java.awt.Dimension;
008import java.awt.GridBagLayout;
009import java.awt.event.ActionEvent;
010import java.awt.event.KeyEvent;
011import java.awt.event.MouseEvent;
012import java.util.ArrayList;
013import java.util.Arrays;
014import java.util.LinkedHashSet;
015import java.util.List;
016import java.util.Set;
017
018import javax.swing.AbstractAction;
019import javax.swing.Box;
020import javax.swing.JComponent;
021import javax.swing.JLabel;
022import javax.swing.JPanel;
023import javax.swing.JPopupMenu;
024import javax.swing.JScrollPane;
025import javax.swing.JSeparator;
026import javax.swing.JTree;
027import javax.swing.event.TreeModelEvent;
028import javax.swing.event.TreeModelListener;
029import javax.swing.event.TreeSelectionEvent;
030import javax.swing.event.TreeSelectionListener;
031import javax.swing.tree.DefaultMutableTreeNode;
032import javax.swing.tree.DefaultTreeCellRenderer;
033import javax.swing.tree.DefaultTreeModel;
034import javax.swing.tree.TreePath;
035import javax.swing.tree.TreeSelectionModel;
036
037import org.openstreetmap.josm.Main;
038import org.openstreetmap.josm.actions.AutoScaleAction;
039import org.openstreetmap.josm.command.Command;
040import org.openstreetmap.josm.command.PseudoCommand;
041import org.openstreetmap.josm.data.osm.OsmPrimitive;
042import org.openstreetmap.josm.gui.SideButton;
043import org.openstreetmap.josm.gui.layer.OsmDataLayer;
044import org.openstreetmap.josm.gui.layer.OsmDataLayer.CommandQueueListener;
045import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
046import org.openstreetmap.josm.tools.FilteredCollection;
047import org.openstreetmap.josm.tools.GBC;
048import org.openstreetmap.josm.tools.ImageProvider;
049import org.openstreetmap.josm.tools.InputMapUtils;
050import org.openstreetmap.josm.tools.Predicate;
051import org.openstreetmap.josm.tools.Shortcut;
052
053/**
054 * Dialog displaying list of all executed commands (undo/redo buffer).
055 * @since 94
056 */
057public class CommandStackDialog extends ToggleDialog implements CommandQueueListener {
058
059    private final DefaultTreeModel undoTreeModel = new DefaultTreeModel(new DefaultMutableTreeNode());
060    private final DefaultTreeModel redoTreeModel = new DefaultTreeModel(new DefaultMutableTreeNode());
061
062    private final JTree undoTree = new JTree(undoTreeModel);
063    private final JTree redoTree = new JTree(redoTreeModel);
064
065    private transient UndoRedoSelectionListener undoSelectionListener;
066    private transient UndoRedoSelectionListener redoSelectionListener;
067
068    private JScrollPane scrollPane;
069    private JSeparator separator = new JSeparator();
070    // only visible, if separator is the top most component
071    private Component spacer = Box.createRigidArea(new Dimension(0, 3));
072
073    // last operation is remembered to select the next undo/redo entry in the list
074    // after undo/redo command
075    private UndoRedoType lastOperation = UndoRedoType.UNDO;
076
077    // Actions for context menu and Enter key
078    private SelectAction selectAction = new SelectAction();
079    private SelectAndZoomAction selectAndZoomAction = new SelectAndZoomAction();
080
081    /**
082     * Constructs a new {@code CommandStackDialog}.
083     */
084    public CommandStackDialog() {
085        super(tr("Command Stack"), "commandstack", tr("Open a list of all commands (undo buffer)."),
086                Shortcut.registerShortcut("subwindow:commandstack", tr("Toggle: {0}",
087                tr("Command Stack")), KeyEvent.VK_O, Shortcut.ALT_SHIFT), 100);
088        undoTree.addMouseListener(new MouseEventHandler());
089        undoTree.setRootVisible(false);
090        undoTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
091        undoTree.setShowsRootHandles(true);
092        undoTree.expandRow(0);
093        undoTree.setCellRenderer(new CommandCellRenderer());
094        undoSelectionListener = new UndoRedoSelectionListener(undoTree);
095        undoTree.getSelectionModel().addTreeSelectionListener(undoSelectionListener);
096        InputMapUtils.unassignCtrlShiftUpDown(undoTree, JComponent.WHEN_FOCUSED);
097
098        redoTree.addMouseListener(new MouseEventHandler());
099        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 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     */
200    protected void wireUpdateEnabledStateUpdater(final IEnabledStateUpdating updater, JTree tree) {
201        addShowNotifyListener(updater);
202
203        tree.addTreeSelectionListener(new TreeSelectionListener() {
204            @Override
205            public void valueChanged(TreeSelectionEvent e) {
206                updater.updateEnabledState();
207            }
208        });
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                updater.updateEnabledState();
220                updateTitle();
221            }
222
223            @Override
224            public void treeNodesRemoved(TreeModelEvent e) {
225                updater.updateEnabledState();
226                updateTitle();
227            }
228
229            @Override
230            public void treeStructureChanged(TreeModelEvent e) {
231                updater.updateEnabledState();
232                updateTitle();
233            }
234        });
235    }
236
237    @Override
238    public void showNotify() {
239        buildTrees();
240        for (IEnabledStateUpdating listener : showNotifyListener) {
241            listener.updateEnabledState();
242        }
243        Main.main.undoRedo.addCommandQueueListener(this);
244    }
245
246    /**
247     * Simple listener setup to update the button enabled state when the side dialog shows.
248     */
249    private transient Set<IEnabledStateUpdating> showNotifyListener = new LinkedHashSet<>();
250
251    private void addShowNotifyListener(IEnabledStateUpdating listener) {
252        showNotifyListener.add(listener);
253    }
254
255    @Override
256    public void hideNotify() {
257        undoTreeModel.setRoot(new DefaultMutableTreeNode());
258        redoTreeModel.setRoot(new DefaultMutableTreeNode());
259        Main.main.undoRedo.removeCommandQueueListener(this);
260    }
261
262    /**
263     * Build the trees of undo and redo commands (initially or when
264     * they have changed).
265     */
266    private void buildTrees() {
267        setTitle(tr("Command Stack"));
268        if (!Main.main.hasEditLayer())
269            return;
270
271        List<Command> undoCommands = Main.main.undoRedo.commands;
272        DefaultMutableTreeNode undoRoot = new DefaultMutableTreeNode();
273        for (int i = 0; i < undoCommands.size(); ++i) {
274            undoRoot.add(getNodeForCommand(undoCommands.get(i), i));
275        }
276        undoTreeModel.setRoot(undoRoot);
277
278        List<Command> redoCommands = Main.main.undoRedo.redoCommands;
279        DefaultMutableTreeNode redoRoot = new DefaultMutableTreeNode();
280        for (int i = 0; i < redoCommands.size(); ++i) {
281            redoRoot.add(getNodeForCommand(redoCommands.get(i), i));
282        }
283        redoTreeModel.setRoot(redoRoot);
284        if (redoTreeModel.getChildCount(redoRoot) > 0) {
285            redoTree.scrollRowToVisible(0);
286            scrollPane.getHorizontalScrollBar().setValue(0);
287        }
288
289        separator.setVisible(!undoCommands.isEmpty() || !redoCommands.isEmpty());
290        spacer.setVisible(undoCommands.isEmpty() && !redoCommands.isEmpty());
291
292        // if one tree is empty, move selection to the other
293        switch (lastOperation) {
294        case UNDO:
295            if (undoCommands.isEmpty()) {
296                lastOperation = UndoRedoType.REDO;
297            }
298            break;
299        case REDO:
300            if (redoCommands.isEmpty()) {
301                lastOperation = UndoRedoType.UNDO;
302            }
303            break;
304        }
305
306        // select the next command to undo/redo
307        switch (lastOperation) {
308        case UNDO:
309            undoTree.setSelectionRow(undoTree.getRowCount()-1);
310            break;
311        case REDO:
312            redoTree.setSelectionRow(0);
313            break;
314        }
315
316        undoTree.scrollRowToVisible(undoTreeModel.getChildCount(undoRoot)-1);
317        scrollPane.getHorizontalScrollBar().setValue(0);
318    }
319
320    /**
321     * Wraps a command in a CommandListMutableTreeNode.
322     * Recursively adds child commands.
323     * @param c the command
324     * @return the resulting node
325     */
326    protected CommandListMutableTreeNode getNodeForCommand(PseudoCommand c, int idx) {
327        CommandListMutableTreeNode node = new CommandListMutableTreeNode(c, idx);
328        if (c.getChildren() != null) {
329            List<PseudoCommand> children = new ArrayList<>(c.getChildren());
330            for (int i = 0; i < children.size(); ++i) {
331                node.add(getNodeForCommand(children.get(i), i));
332            }
333        }
334        return node;
335    }
336
337    /**
338     * Return primitives that are affected by some command
339     * @param path GUI elements
340     * @return collection of affected primitives, onluy usable ones
341     */
342    protected static FilteredCollection<? extends OsmPrimitive> getAffectedPrimitives(TreePath path) {
343        PseudoCommand c = ((CommandListMutableTreeNode) path.getLastPathComponent()).getCommand();
344        final OsmDataLayer currentLayer = Main.main.getEditLayer();
345        return new FilteredCollection<>(
346                c.getParticipatingPrimitives(),
347                new Predicate<OsmPrimitive>() {
348                    @Override
349                    public boolean evaluate(OsmPrimitive o) {
350                        OsmPrimitive p = currentLayer.data.getPrimitiveById(o);
351                        return p != null && p.isUsable();
352                    }
353                }
354        );
355    }
356
357    @Override
358    public void commandChanged(int queueSize, int redoSize) {
359        if (!isVisible())
360            return;
361        buildTrees();
362    }
363
364    /**
365     * Action that selects the objects that take part in a command.
366     */
367    public class SelectAction extends AbstractAction implements IEnabledStateUpdating {
368
369        /**
370         * Constructs a new {@code SelectAction}.
371         */
372        public SelectAction() {
373            putValue(NAME, tr("Select"));
374            putValue(SHORT_DESCRIPTION, tr("Selects the objects that take part in this command (unless currently deleted)"));
375            putValue(SMALL_ICON, ImageProvider.get("dialogs", "select"));
376        }
377
378        @Override
379        public void actionPerformed(ActionEvent e) {
380            TreePath path;
381            if (!undoTree.isSelectionEmpty()) {
382                path = undoTree.getSelectionPath();
383            } else if (!redoTree.isSelectionEmpty()) {
384                path = redoTree.getSelectionPath();
385            } else
386                throw new IllegalStateException();
387
388            OsmDataLayer editLayer = Main.main.getEditLayer();
389            if (editLayer == null) return;
390            editLayer.data.setSelected(getAffectedPrimitives(path));
391        }
392
393        @Override
394        public void updateEnabledState() {
395            setEnabled(!undoTree.isSelectionEmpty() || !redoTree.isSelectionEmpty());
396        }
397    }
398
399    /**
400     * Action that selects the objects that take part in a command, then zoom to them.
401     */
402    public class SelectAndZoomAction extends SelectAction {
403        /**
404         * Constructs a new {@code SelectAndZoomAction}.
405         */
406        public SelectAndZoomAction() {
407            putValue(NAME, tr("Select and zoom"));
408            putValue(SHORT_DESCRIPTION,
409                    tr("Selects the objects that take part in this command (unless currently deleted), then and zooms to it"));
410            putValue(SMALL_ICON, ImageProvider.get("dialogs/autoscale", "selection"));
411        }
412
413        @Override
414        public void actionPerformed(ActionEvent e) {
415            super.actionPerformed(e);
416            if (!Main.main.hasEditLayer()) return;
417            AutoScaleAction.autoScale("selection");
418        }
419    }
420
421    /**
422     * undo / redo switch to reduce duplicate code
423     */
424    protected enum UndoRedoType {
425        UNDO,
426        REDO
427    }
428
429    /**
430     * Action to undo or redo all commands up to (and including) the seleced item.
431     */
432    protected class UndoRedoAction extends AbstractAction implements IEnabledStateUpdating {
433        private UndoRedoType type;
434        private JTree tree;
435
436        /**
437         * constructor
438         * @param type decide whether it is an undo action or a redo action
439         */
440        public UndoRedoAction(UndoRedoType type) {
441            super();
442            this.type = type;
443            switch (type) {
444            case UNDO:
445                tree = undoTree;
446                putValue(NAME, tr("Undo"));
447                putValue(SHORT_DESCRIPTION, tr("Undo the selected and all later commands"));
448                putValue(SMALL_ICON, ImageProvider.get("undo"));
449                break;
450            case REDO:
451                tree = redoTree;
452                putValue(NAME, tr("Redo"));
453                putValue(SHORT_DESCRIPTION, tr("Redo the selected and all earlier commands"));
454                putValue(SMALL_ICON, ImageProvider.get("redo"));
455                break;
456            }
457        }
458
459        @Override
460        public void actionPerformed(ActionEvent e) {
461            lastOperation = type;
462            TreePath path = tree.getSelectionPath();
463
464            // we can only undo top level commands
465            if (path.getPathCount() != 2)
466                throw new IllegalStateException();
467
468            int idx = ((CommandListMutableTreeNode) path.getLastPathComponent()).getIndex();
469
470            // calculate the number of commands to undo/redo; then do it
471            switch (type) {
472            case UNDO:
473                int numUndo = ((DefaultMutableTreeNode) undoTreeModel.getRoot()).getChildCount() - idx;
474                Main.main.undoRedo.undo(numUndo);
475                break;
476            case REDO:
477                int numRedo = idx+1;
478                Main.main.undoRedo.redo(numRedo);
479                break;
480            }
481            Main.map.repaint();
482        }
483
484        @Override
485        public void updateEnabledState() {
486            // do not allow execution if nothing is selected or a sub command was selected
487            setEnabled(!tree.isSelectionEmpty() && tree.getSelectionPath().getPathCount() == 2);
488        }
489    }
490
491    class MouseEventHandler extends PopupMenuLauncher {
492
493        MouseEventHandler() {
494            super(new CommandStackPopup());
495        }
496
497        @Override
498        public void mouseClicked(MouseEvent evt) {
499            if (isDoubleClick(evt)) {
500                selectAndZoomAction.actionPerformed(null);
501            }
502        }
503    }
504
505    private class CommandStackPopup extends JPopupMenu {
506        CommandStackPopup() {
507            add(selectAction);
508            add(selectAndZoomAction);
509        }
510    }
511}