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