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
053public class CommandStackDialog extends ToggleDialog implements CommandQueueListener {
054
055    private DefaultTreeModel undoTreeModel = new DefaultTreeModel(new DefaultMutableTreeNode());
056    private DefaultTreeModel redoTreeModel = new DefaultTreeModel(new DefaultMutableTreeNode());
057
058    private JTree undoTree = new JTree(undoTreeModel);
059    private JTree redoTree = new JTree(redoTreeModel);
060
061    private UndoRedoSelectionListener undoSelectionListener;
062    private UndoRedoSelectionListener redoSelectionListener;
063
064    private JScrollPane scrollPane;
065    private JSeparator separator = new JSeparator();
066    // only visible, if separator is the top most component
067    private Component spacer = Box.createRigidArea(new Dimension(0, 3));
068
069    // last operation is remembered to select the next undo/redo entry in the list
070    // after undo/redo command
071    private UndoRedoType lastOperation = UndoRedoType.UNDO;
072
073    // Actions for context menu and Enter key
074    private SelectAction selectAction = new SelectAction();
075    private SelectAndZoomAction selectAndZoomAction = new SelectAndZoomAction();
076
077    /**
078     * Constructs a new {@code CommandStackDialog}.
079     */
080    public CommandStackDialog() {
081        super(tr("Command Stack"), "commandstack", tr("Open a list of all commands (undo buffer)."),
082                Shortcut.registerShortcut("subwindow:commandstack", tr("Toggle: {0}",
083                tr("Command Stack")), KeyEvent.VK_O, Shortcut.ALT_SHIFT), 100);
084        undoTree.addMouseListener(new MouseEventHandler());
085        undoTree.setRootVisible(false);
086        undoTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
087        undoTree.setShowsRootHandles(true);
088        undoTree.expandRow(0);
089        undoTree.setCellRenderer(new CommandCellRenderer());
090        undoSelectionListener = new UndoRedoSelectionListener(undoTree);
091        undoTree.getSelectionModel().addTreeSelectionListener(undoSelectionListener);
092        InputMapUtils.unassignCtrlShiftUpDown(undoTree, JComponent.WHEN_FOCUSED);
093
094        redoTree.addMouseListener(new MouseEventHandler());
095        redoTree.setRootVisible(false);
096        redoTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
097        redoTree.setShowsRootHandles(true);
098        redoTree.expandRow(0);
099        redoTree.setCellRenderer(new CommandCellRenderer());
100        redoSelectionListener = new UndoRedoSelectionListener(redoTree);
101        redoTree.getSelectionModel().addTreeSelectionListener(redoSelectionListener);
102
103        JPanel treesPanel = new JPanel(new GridBagLayout());
104
105        treesPanel.add(spacer, GBC.eol());
106        spacer.setVisible(false);
107        treesPanel.add(undoTree, GBC.eol().fill(GBC.HORIZONTAL));
108        separator.setVisible(false);
109        treesPanel.add(separator, GBC.eol().fill(GBC.HORIZONTAL));
110        treesPanel.add(redoTree, GBC.eol().fill(GBC.HORIZONTAL));
111        treesPanel.add(Box.createRigidArea(new Dimension(0, 0)), GBC.std().weight(0, 1));
112        treesPanel.setBackground(redoTree.getBackground());
113
114        wireUpdateEnabledStateUpdater(selectAction, undoTree);
115        wireUpdateEnabledStateUpdater(selectAction, redoTree);
116
117        UndoRedoAction undoAction = new UndoRedoAction(UndoRedoType.UNDO);
118        wireUpdateEnabledStateUpdater(undoAction, undoTree);
119
120        UndoRedoAction redoAction = new UndoRedoAction(UndoRedoType.REDO);
121        wireUpdateEnabledStateUpdater(redoAction, redoTree);
122
123        scrollPane = (JScrollPane)createLayout(treesPanel, true, Arrays.asList(new SideButton[] {
124            new SideButton(selectAction),
125            new SideButton(undoAction),
126            new SideButton(redoAction)
127        }));
128
129        InputMapUtils.addEnterAction(undoTree, selectAndZoomAction);
130        InputMapUtils.addEnterAction(redoTree, selectAndZoomAction);
131    }
132
133    private static class CommandCellRenderer extends DefaultTreeCellRenderer {
134        @Override public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) {
135            super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
136            DefaultMutableTreeNode v = (DefaultMutableTreeNode)value;
137            if (v.getUserObject() instanceof JLabel) {
138                JLabel l = (JLabel)v.getUserObject();
139                setIcon(l.getIcon());
140                setText(l.getText());
141            }
142            return this;
143        }
144    }
145
146    /**
147     * Selection listener for undo and redo area.
148     * If one is clicked, takes away the selection from the other, so
149     * it behaves as if it was one component.
150     */
151    private class UndoRedoSelectionListener implements TreeSelectionListener {
152        private JTree source;
153
154        public UndoRedoSelectionListener(JTree source) {
155            this.source = source;
156        }
157
158        @Override
159        public void valueChanged(TreeSelectionEvent e) {
160            if (source == undoTree) {
161                redoTree.getSelectionModel().removeTreeSelectionListener(redoSelectionListener);
162                redoTree.clearSelection();
163                redoTree.getSelectionModel().addTreeSelectionListener(redoSelectionListener);
164            }
165            if (source == redoTree) {
166                undoTree.getSelectionModel().removeTreeSelectionListener(undoSelectionListener);
167                undoTree.clearSelection();
168                undoTree.getSelectionModel().addTreeSelectionListener(undoSelectionListener);
169            }
170        }
171    }
172
173    /**
174     * Interface to provide a callback for enabled state update.
175     */
176    protected interface IEnabledStateUpdating {
177        void updateEnabledState();
178    }
179
180    /**
181     * Wires updater for enabled state to the events.
182     */
183    protected void wireUpdateEnabledStateUpdater(final IEnabledStateUpdating updater, JTree tree) {
184        addShowNotifyListener(updater);
185
186        tree.addTreeSelectionListener(new TreeSelectionListener() {
187            @Override
188            public void valueChanged(TreeSelectionEvent e) {
189                updater.updateEnabledState();
190            }
191        });
192
193        tree.getModel().addTreeModelListener(new TreeModelListener() {
194            @Override
195            public void treeNodesChanged(TreeModelEvent e) {
196                updater.updateEnabledState();
197            }
198
199            @Override
200            public void treeNodesInserted(TreeModelEvent e) {
201                updater.updateEnabledState();
202            }
203
204            @Override
205            public void treeNodesRemoved(TreeModelEvent e) {
206                updater.updateEnabledState();
207            }
208
209            @Override
210            public void treeStructureChanged(TreeModelEvent e) {
211                updater.updateEnabledState();
212            }
213        });
214    }
215
216    @Override
217    public void showNotify() {
218        buildTrees();
219        for (IEnabledStateUpdating listener : showNotifyListener) {
220            listener.updateEnabledState();
221        }
222        Main.main.undoRedo.addCommandQueueListener(this);
223    }
224
225    /**
226     * Simple listener setup to update the button enabled state when the side dialog shows.
227     */
228    Set<IEnabledStateUpdating> showNotifyListener = new LinkedHashSet<>();
229
230    private void addShowNotifyListener(IEnabledStateUpdating listener) {
231        showNotifyListener.add(listener);
232    }
233
234    @Override
235    public void hideNotify() {
236        undoTreeModel.setRoot(new DefaultMutableTreeNode());
237        redoTreeModel.setRoot(new DefaultMutableTreeNode());
238        Main.main.undoRedo.removeCommandQueueListener(this);
239    }
240
241    /**
242     * Build the trees of undo and redo commands (initially or when
243     * they have changed).
244     */
245    private void buildTrees() {
246        setTitle(tr("Command Stack"));
247        if (!Main.main.hasEditLayer())
248            return;
249
250        List<Command> undoCommands = Main.main.undoRedo.commands;
251        DefaultMutableTreeNode undoRoot = new DefaultMutableTreeNode();
252        for (int i=0; i<undoCommands.size(); ++i) {
253            undoRoot.add(getNodeForCommand(undoCommands.get(i), i));
254        }
255        undoTreeModel.setRoot(undoRoot);
256
257        List<Command> redoCommands = Main.main.undoRedo.redoCommands;
258        DefaultMutableTreeNode redoRoot = new DefaultMutableTreeNode();
259        for (int i=0; i<redoCommands.size(); ++i) {
260            redoRoot.add(getNodeForCommand(redoCommands.get(i), i));
261        }
262        redoTreeModel.setRoot(redoRoot);
263        if (redoTreeModel.getChildCount(redoRoot) > 0) {
264            redoTree.scrollRowToVisible(0);
265            scrollPane.getHorizontalScrollBar().setValue(0);
266        }
267
268        separator.setVisible(!undoCommands.isEmpty() || !redoCommands.isEmpty());
269        spacer.setVisible(undoCommands.isEmpty() && !redoCommands.isEmpty());
270
271        // if one tree is empty, move selection to the other
272        switch (lastOperation) {
273        case UNDO:
274            if (undoCommands.isEmpty()) {
275                lastOperation = UndoRedoType.REDO;
276            }
277            break;
278        case REDO:
279            if (redoCommands.isEmpty()) {
280                lastOperation = UndoRedoType.UNDO;
281            }
282            break;
283        }
284
285        // select the next command to undo/redo
286        switch (lastOperation) {
287        case UNDO:
288            undoTree.setSelectionRow(undoTree.getRowCount()-1);
289            break;
290        case REDO:
291            redoTree.setSelectionRow(0);
292            break;
293        }
294
295        undoTree.scrollRowToVisible(undoTreeModel.getChildCount(undoRoot)-1);
296        scrollPane.getHorizontalScrollBar().setValue(0);
297    }
298
299    /**
300     * Wraps a command in a CommandListMutableTreeNode.
301     * Recursively adds child commands.
302     */
303    protected CommandListMutableTreeNode getNodeForCommand(PseudoCommand c, int idx) {
304        CommandListMutableTreeNode node = new CommandListMutableTreeNode(c, idx);
305        if (c.getChildren() != null) {
306            List<PseudoCommand> children = new ArrayList<>(c.getChildren());
307            for (int i=0; i<children.size(); ++i) {
308                node.add(getNodeForCommand(children.get(i), i));
309            }
310        }
311        return node;
312    }
313
314    /**
315     * Return primitives that are affected by some command
316     * @param path GUI elements
317     * @return collection of affected primitives, onluy usable ones
318     */
319    protected static FilteredCollection<OsmPrimitive> getAffectedPrimitives(TreePath path) {
320        PseudoCommand c = ((CommandListMutableTreeNode) path.getLastPathComponent()).getCommand();
321        final OsmDataLayer currentLayer = Main.main.getEditLayer();
322        return new FilteredCollection<>(
323                c.getParticipatingPrimitives(),
324                new Predicate<OsmPrimitive>(){
325                    @Override
326                    public boolean evaluate(OsmPrimitive o) {
327                        OsmPrimitive p = currentLayer.data.getPrimitiveById(o);
328                        return p != null && p.isUsable();
329                    }
330                }
331        );
332    }
333
334    @Override
335    public void commandChanged(int queueSize, int redoSize) {
336        if (!isVisible())
337            return;
338        buildTrees();
339    }
340
341    public class SelectAction extends AbstractAction implements IEnabledStateUpdating {
342
343        /**
344         * Constructs a new {@code SelectAction}.
345         */
346        public SelectAction() {
347            putValue(NAME,tr("Select"));
348            putValue(SHORT_DESCRIPTION, tr("Selects the objects that take part in this command (unless currently deleted)"));
349            putValue(SMALL_ICON, ImageProvider.get("dialogs","select"));
350        }
351
352        @Override
353        public void actionPerformed(ActionEvent e) {
354            TreePath path;
355            undoTree.getSelectionPath();
356            if (!undoTree.isSelectionEmpty()) {
357                path = undoTree.getSelectionPath();
358            } else if (!redoTree.isSelectionEmpty()) {
359                path = redoTree.getSelectionPath();
360            } else
361                throw new IllegalStateException();
362
363            OsmDataLayer editLayer = Main.main.getEditLayer();
364            if (editLayer == null) return;
365            editLayer.data.setSelected( getAffectedPrimitives(path));
366        }
367
368        @Override
369        public void updateEnabledState() {
370            setEnabled(!undoTree.isSelectionEmpty() || !redoTree.isSelectionEmpty());
371        }
372    }
373
374    public class SelectAndZoomAction extends SelectAction {
375        /**
376         * Constructs a new {@code SelectAndZoomAction}.
377         */
378        public SelectAndZoomAction() {
379            putValue(NAME,tr("Select and zoom"));
380            putValue(SHORT_DESCRIPTION, tr("Selects the objects that take part in this command (unless currently deleted), then and zooms to it"));
381            putValue(SMALL_ICON, ImageProvider.get("dialogs/autoscale","selection"));
382        }
383
384        @Override
385        public void actionPerformed(ActionEvent e) {
386            super.actionPerformed(e);
387            if (!Main.main.hasEditLayer()) return;
388            AutoScaleAction.autoScale("selection");
389        }
390    }
391
392    /**
393     * undo / redo switch to reduce duplicate code
394     */
395    protected enum UndoRedoType {UNDO, REDO}
396
397    /**
398     * Action to undo or redo all commands up to (and including) the seleced item.
399     */
400    protected class UndoRedoAction extends AbstractAction implements IEnabledStateUpdating {
401        private UndoRedoType type;
402        private JTree tree;
403
404        /**
405         * constructor
406         * @param type decide whether it is an undo action or a redo action
407         */
408        public UndoRedoAction(UndoRedoType type) {
409            super();
410            this.type = type;
411            switch (type) {
412            case UNDO:
413                tree = undoTree;
414                putValue(NAME,tr("Undo"));
415                putValue(SHORT_DESCRIPTION, tr("Undo the selected and all later commands"));
416                putValue(SMALL_ICON, ImageProvider.get("undo"));
417                break;
418            case REDO:
419                tree = redoTree;
420                putValue(NAME,tr("Redo"));
421                putValue(SHORT_DESCRIPTION, tr("Redo the selected and all earlier commands"));
422                putValue(SMALL_ICON, ImageProvider.get("redo"));
423                break;
424            }
425        }
426
427        @Override
428        public void actionPerformed(ActionEvent e) {
429            lastOperation = type;
430            TreePath path = tree.getSelectionPath();
431
432            // we can only undo top level commands
433            if (path.getPathCount() != 2)
434                throw new IllegalStateException();
435
436            int idx = ((CommandListMutableTreeNode) path.getLastPathComponent()).getIndex();
437
438            // calculate the number of commands to undo/redo; then do it
439            switch (type) {
440            case UNDO:
441                int numUndo = ((DefaultMutableTreeNode) undoTreeModel.getRoot()).getChildCount() - idx;
442                Main.main.undoRedo.undo(numUndo);
443                break;
444            case REDO:
445                int numRedo = idx+1;
446                Main.main.undoRedo.redo(numRedo);
447                break;
448            }
449            Main.map.repaint();
450        }
451
452        @Override
453        public void updateEnabledState() {
454            // do not allow execution if nothing is selected or a sub command was selected
455            setEnabled(!tree.isSelectionEmpty() && tree.getSelectionPath().getPathCount()==2);
456        }
457    }
458
459    class MouseEventHandler extends PopupMenuLauncher {
460
461        public MouseEventHandler() {
462            super(new CommandStackPopup());
463        }
464
465        @Override
466        public void mouseClicked(MouseEvent evt) {
467            if (isDoubleClick(evt)) {
468                selectAndZoomAction.actionPerformed(null);
469            }
470        }
471    }
472
473    private class CommandStackPopup extends JPopupMenu {
474        public CommandStackPopup(){
475            add(selectAction);
476            add(selectAndZoomAction);
477        }
478    }
479}