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 final transient UndoRedoSelectionListener undoSelectionListener;
066    private final transient UndoRedoSelectionListener redoSelectionListener;
067
068    private final JScrollPane scrollPane;
069    private final JSeparator separator = new JSeparator();
070    // only visible, if separator is the top most component
071    private final 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 final SelectAction selectAction = new SelectAction();
079    private final 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 final 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     * @param updater updater
200     * @param tree tree on which wire updater
201     */
202    protected void wireUpdateEnabledStateUpdater(final IEnabledStateUpdating updater, JTree tree) {
203        addShowNotifyListener(updater);
204
205        tree.addTreeSelectionListener(new TreeSelectionListener() {
206            @Override
207            public void valueChanged(TreeSelectionEvent e) {
208                updater.updateEnabledState();
209            }
210        });
211
212        tree.getModel().addTreeModelListener(new TreeModelListener() {
213            @Override
214            public void treeNodesChanged(TreeModelEvent e) {
215                updater.updateEnabledState();
216                updateTitle();
217            }
218
219            @Override
220            public void treeNodesInserted(TreeModelEvent e) {
221                updater.updateEnabledState();
222                updateTitle();
223            }
224
225            @Override
226            public void treeNodesRemoved(TreeModelEvent e) {
227                updater.updateEnabledState();
228                updateTitle();
229            }
230
231            @Override
232            public void treeStructureChanged(TreeModelEvent e) {
233                updater.updateEnabledState();
234                updateTitle();
235            }
236        });
237    }
238
239    @Override
240    public void showNotify() {
241        buildTrees();
242        for (IEnabledStateUpdating listener : showNotifyListener) {
243            listener.updateEnabledState();
244        }
245        Main.main.undoRedo.addCommandQueueListener(this);
246    }
247
248    /**
249     * Simple listener setup to update the button enabled state when the side dialog shows.
250     */
251    private final transient Set<IEnabledStateUpdating> showNotifyListener = new LinkedHashSet<>();
252
253    private void addShowNotifyListener(IEnabledStateUpdating listener) {
254        showNotifyListener.add(listener);
255    }
256
257    @Override
258    public void hideNotify() {
259        undoTreeModel.setRoot(new DefaultMutableTreeNode());
260        redoTreeModel.setRoot(new DefaultMutableTreeNode());
261        Main.main.undoRedo.removeCommandQueueListener(this);
262    }
263
264    /**
265     * Build the trees of undo and redo commands (initially or when
266     * they have changed).
267     */
268    private void buildTrees() {
269        setTitle(tr("Command Stack"));
270        if (!Main.main.hasEditLayer())
271            return;
272
273        List<Command> undoCommands = Main.main.undoRedo.commands;
274        DefaultMutableTreeNode undoRoot = new DefaultMutableTreeNode();
275        for (int i = 0; i < undoCommands.size(); ++i) {
276            undoRoot.add(getNodeForCommand(undoCommands.get(i), i));
277        }
278        undoTreeModel.setRoot(undoRoot);
279
280        List<Command> redoCommands = Main.main.undoRedo.redoCommands;
281        DefaultMutableTreeNode redoRoot = new DefaultMutableTreeNode();
282        for (int i = 0; i < redoCommands.size(); ++i) {
283            redoRoot.add(getNodeForCommand(redoCommands.get(i), i));
284        }
285        redoTreeModel.setRoot(redoRoot);
286        if (redoTreeModel.getChildCount(redoRoot) > 0) {
287            redoTree.scrollRowToVisible(0);
288            scrollPane.getHorizontalScrollBar().setValue(0);
289        }
290
291        separator.setVisible(!undoCommands.isEmpty() || !redoCommands.isEmpty());
292        spacer.setVisible(undoCommands.isEmpty() && !redoCommands.isEmpty());
293
294        // if one tree is empty, move selection to the other
295        switch (lastOperation) {
296        case UNDO:
297            if (undoCommands.isEmpty()) {
298                lastOperation = UndoRedoType.REDO;
299            }
300            break;
301        case REDO:
302            if (redoCommands.isEmpty()) {
303                lastOperation = UndoRedoType.UNDO;
304            }
305            break;
306        }
307
308        // select the next command to undo/redo
309        switch (lastOperation) {
310        case UNDO:
311            undoTree.setSelectionRow(undoTree.getRowCount()-1);
312            break;
313        case REDO:
314            redoTree.setSelectionRow(0);
315            break;
316        }
317
318        undoTree.scrollRowToVisible(undoTreeModel.getChildCount(undoRoot)-1);
319        scrollPane.getHorizontalScrollBar().setValue(0);
320    }
321
322    /**
323     * Wraps a command in a CommandListMutableTreeNode.
324     * Recursively adds child commands.
325     * @param c the command
326     * @param idx index
327     * @return the resulting node
328     */
329    protected CommandListMutableTreeNode getNodeForCommand(PseudoCommand c, int idx) {
330        CommandListMutableTreeNode node = new CommandListMutableTreeNode(c, idx);
331        if (c.getChildren() != null) {
332            List<PseudoCommand> children = new ArrayList<>(c.getChildren());
333            for (int i = 0; i < children.size(); ++i) {
334                node.add(getNodeForCommand(children.get(i), i));
335            }
336        }
337        return node;
338    }
339
340    /**
341     * Return primitives that are affected by some command
342     * @param path GUI elements
343     * @return collection of affected primitives, onluy usable ones
344     */
345    protected static FilteredCollection<? extends OsmPrimitive> getAffectedPrimitives(TreePath path) {
346        PseudoCommand c = ((CommandListMutableTreeNode) path.getLastPathComponent()).getCommand();
347        final OsmDataLayer currentLayer = Main.main.getEditLayer();
348        return new FilteredCollection<>(
349                c.getParticipatingPrimitives(),
350                new Predicate<OsmPrimitive>() {
351                    @Override
352                    public boolean evaluate(OsmPrimitive o) {
353                        OsmPrimitive p = currentLayer.data.getPrimitiveById(o);
354                        return p != null && p.isUsable();
355                    }
356                }
357        );
358    }
359
360    @Override
361    public void commandChanged(int queueSize, int redoSize) {
362        if (!isVisible())
363            return;
364        buildTrees();
365    }
366
367    /**
368     * Action that selects the objects that take part in a command.
369     */
370    public class SelectAction extends AbstractAction implements IEnabledStateUpdating {
371
372        /**
373         * Constructs a new {@code SelectAction}.
374         */
375        public SelectAction() {
376            putValue(NAME, tr("Select"));
377            putValue(SHORT_DESCRIPTION, tr("Selects the objects that take part in this command (unless currently deleted)"));
378            putValue(SMALL_ICON, ImageProvider.get("dialogs", "select"));
379        }
380
381        @Override
382        public void actionPerformed(ActionEvent e) {
383            TreePath path;
384            if (!undoTree.isSelectionEmpty()) {
385                path = undoTree.getSelectionPath();
386            } else if (!redoTree.isSelectionEmpty()) {
387                path = redoTree.getSelectionPath();
388            } else
389                throw new IllegalStateException();
390
391            OsmDataLayer editLayer = Main.main.getEditLayer();
392            if (editLayer == null) return;
393            editLayer.data.setSelected(getAffectedPrimitives(path));
394        }
395
396        @Override
397        public void updateEnabledState() {
398            setEnabled(!undoTree.isSelectionEmpty() || !redoTree.isSelectionEmpty());
399        }
400    }
401
402    /**
403     * Action that selects the objects that take part in a command, then zoom to them.
404     */
405    public class SelectAndZoomAction extends SelectAction {
406        /**
407         * Constructs a new {@code SelectAndZoomAction}.
408         */
409        public SelectAndZoomAction() {
410            putValue(NAME, tr("Select and zoom"));
411            putValue(SHORT_DESCRIPTION,
412                    tr("Selects the objects that take part in this command (unless currently deleted), then and zooms to it"));
413            putValue(SMALL_ICON, ImageProvider.get("dialogs/autoscale", "selection"));
414        }
415
416        @Override
417        public void actionPerformed(ActionEvent e) {
418            super.actionPerformed(e);
419            if (!Main.main.hasEditLayer()) return;
420            AutoScaleAction.autoScale("selection");
421        }
422    }
423
424    /**
425     * undo / redo switch to reduce duplicate code
426     */
427    protected enum UndoRedoType {
428        UNDO,
429        REDO
430    }
431
432    /**
433     * Action to undo or redo all commands up to (and including) the seleced item.
434     */
435    protected class UndoRedoAction extends AbstractAction implements IEnabledStateUpdating {
436        private final UndoRedoType type;
437        private final JTree tree;
438
439        /**
440         * constructor
441         * @param type decide whether it is an undo action or a redo action
442         */
443        public UndoRedoAction(UndoRedoType type) {
444            this.type = type;
445            if (UndoRedoType.UNDO == type) {
446                tree = undoTree;
447                putValue(NAME, tr("Undo"));
448                putValue(SHORT_DESCRIPTION, tr("Undo the selected and all later commands"));
449                putValue(SMALL_ICON, ImageProvider.get("undo"));
450            } else {
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            }
456        }
457
458        @Override
459        public void actionPerformed(ActionEvent e) {
460            lastOperation = type;
461            TreePath path = tree.getSelectionPath();
462
463            // we can only undo top level commands
464            if (path.getPathCount() != 2)
465                throw new IllegalStateException();
466
467            int idx = ((CommandListMutableTreeNode) path.getLastPathComponent()).getIndex();
468
469            // calculate the number of commands to undo/redo; then do it
470            switch (type) {
471            case UNDO:
472                int numUndo = ((DefaultMutableTreeNode) undoTreeModel.getRoot()).getChildCount() - idx;
473                Main.main.undoRedo.undo(numUndo);
474                break;
475            case REDO:
476                int numRedo = idx+1;
477                Main.main.undoRedo.redo(numRedo);
478                break;
479            }
480            Main.map.repaint();
481        }
482
483        @Override
484        public void updateEnabledState() {
485            // do not allow execution if nothing is selected or a sub command was selected
486            setEnabled(!tree.isSelectionEmpty() && tree.getSelectionPath().getPathCount() == 2);
487        }
488    }
489
490    class MouseEventHandler extends PopupMenuLauncher {
491
492        MouseEventHandler() {
493            super(new CommandStackPopup());
494        }
495
496        @Override
497        public void mouseClicked(MouseEvent evt) {
498            if (isDoubleClick(evt)) {
499                selectAndZoomAction.actionPerformed(null);
500            }
501        }
502    }
503
504    private class CommandStackPopup extends JPopupMenu {
505        CommandStackPopup() {
506            add(selectAction);
507            add(selectAndZoomAction);
508        }
509    }
510}