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, true);
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<IEnabledStateUpdating>();
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<PseudoCommand>(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        FilteredCollection<OsmPrimitive> prims = new FilteredCollection<OsmPrimitive>(
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        return prims;
333    }
334
335    @Override
336    public void commandChanged(int queueSize, int redoSize) {
337        if (!isVisible())
338            return;
339        buildTrees();
340    }
341
342    public class SelectAction extends AbstractAction implements IEnabledStateUpdating {
343
344        /**
345         * Constructs a new {@code SelectAction}.
346         */
347        public SelectAction() {
348            putValue(NAME,tr("Select"));
349            putValue(SHORT_DESCRIPTION, tr("Selects the objects that take part in this command (unless currently deleted)"));
350            putValue(SMALL_ICON, ImageProvider.get("dialogs","select"));
351        }
352
353        @Override
354        public void actionPerformed(ActionEvent e) {
355            TreePath path;
356            undoTree.getSelectionPath();
357            if (!undoTree.isSelectionEmpty()) {
358                path = undoTree.getSelectionPath();
359            } else if (!redoTree.isSelectionEmpty()) {
360                path = redoTree.getSelectionPath();
361            } else
362                throw new IllegalStateException();
363
364            OsmDataLayer editLayer = Main.main.getEditLayer();
365            if (editLayer == null) return;
366            editLayer.data.setSelected( getAffectedPrimitives(path));
367        }
368
369        @Override
370        public void updateEnabledState() {
371            setEnabled(!undoTree.isSelectionEmpty() || !redoTree.isSelectionEmpty());
372        }
373    }
374
375    public class SelectAndZoomAction extends SelectAction {
376        /**
377         * Constructs a new {@code SelectAndZoomAction}.
378         */
379        public SelectAndZoomAction() {
380            putValue(NAME,tr("Select and zoom"));
381            putValue(SHORT_DESCRIPTION, tr("Selects the objects that take part in this command (unless currently deleted), then and zooms to it"));
382            putValue(SMALL_ICON, ImageProvider.get("dialogs/autoscale","selection"));
383        }
384
385        @Override
386        public void actionPerformed(ActionEvent e) {
387            super.actionPerformed(e);
388            if (!Main.main.hasEditLayer()) return;
389            AutoScaleAction.autoScale("selection");
390        }
391    }
392
393    /**
394     * undo / redo switch to reduce duplicate code
395     */
396    protected enum UndoRedoType {UNDO, REDO}
397
398    /**
399     * Action to undo or redo all commands up to (and including) the seleced item.
400     */
401    protected class UndoRedoAction extends AbstractAction implements IEnabledStateUpdating {
402        private UndoRedoType type;
403        private JTree tree;
404
405        /**
406         * constructor
407         * @param type decide whether it is an undo action or a redo action
408         */
409        public UndoRedoAction(UndoRedoType type) {
410            super();
411            this.type = type;
412            switch (type) {
413            case UNDO:
414                tree = undoTree;
415                putValue(NAME,tr("Undo"));
416                putValue(SHORT_DESCRIPTION, tr("Undo the selected and all later commands"));
417                putValue(SMALL_ICON, ImageProvider.get("undo"));
418                break;
419            case REDO:
420                tree = redoTree;
421                putValue(NAME,tr("Redo"));
422                putValue(SHORT_DESCRIPTION, tr("Redo the selected and all earlier commands"));
423                putValue(SMALL_ICON, ImageProvider.get("redo"));
424                break;
425            }
426        }
427
428        @Override
429        public void actionPerformed(ActionEvent e) {
430            lastOperation = type;
431            TreePath path = tree.getSelectionPath();
432
433            // we can only undo top level commands
434            if (path.getPathCount() != 2)
435                throw new IllegalStateException();
436
437            int idx = ((CommandListMutableTreeNode) path.getLastPathComponent()).getIndex();
438
439            // calculate the number of commands to undo/redo; then do it
440            switch (type) {
441            case UNDO:
442                int numUndo = ((DefaultMutableTreeNode) undoTreeModel.getRoot()).getChildCount() - idx;
443                Main.main.undoRedo.undo(numUndo);
444                break;
445            case REDO:
446                int numRedo = idx+1;
447                Main.main.undoRedo.redo(numRedo);
448                break;
449            }
450            Main.map.repaint();
451        }
452
453        @Override
454        public void updateEnabledState() {
455            // do not allow execution if nothing is selected or a sub command was selected
456            setEnabled(!tree.isSelectionEmpty() && tree.getSelectionPath().getPathCount()==2);
457        }
458    }
459
460    class MouseEventHandler extends PopupMenuLauncher {
461
462        public MouseEventHandler() {
463            super(new CommandStackPopup());
464        }
465
466        @Override
467        public void mouseClicked(MouseEvent evt) {
468            if (isDoubleClick(evt)) {
469                selectAndZoomAction.actionPerformed(null);
470            }
471        }
472    }
473
474    private class CommandStackPopup extends JPopupMenu {
475        public CommandStackPopup(){
476            add(selectAction);
477            add(selectAndZoomAction);
478        }
479    }
480}