source: josm/trunk/src/org/openstreetmap/josm/gui/dialogs/CommandStackDialog.java @ 5241

Revision 5200, 16.8 KB checked in by akks, 5 weeks ago (diff)

see #7626, fix #7463: keys Ctrl-Shift-Up/Down, Enter, Spacebar work better in toggle dialogs
Enter and Spacebar = useful actions for list items (select, toggle, etc.)

  • Property svn:eol-style set to native
Line 
1// License: GPL. Copyright 2007 by Immanuel Scholz and others
2package org.openstreetmap.josm.gui.dialogs;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.BorderLayout;
7import java.awt.Component;
8import java.awt.Dimension;
9import java.awt.GridBagLayout;
10import java.awt.Point;
11import java.awt.event.ActionEvent;
12import java.awt.event.KeyEvent;
13import java.awt.event.MouseEvent;
14import java.util.ArrayList;
15import java.util.Arrays;
16import java.util.LinkedHashSet;
17import java.util.List;
18import java.util.Set;
19
20import javax.swing.AbstractAction;
21import javax.swing.Box;
22import javax.swing.JComponent;
23import javax.swing.JLabel;
24import javax.swing.JPanel;
25import javax.swing.JPopupMenu;
26import javax.swing.JScrollPane;
27import javax.swing.JSeparator;
28import javax.swing.JTree;
29import javax.swing.event.TreeModelEvent;
30import javax.swing.event.TreeModelListener;
31import javax.swing.event.TreeSelectionEvent;
32import javax.swing.event.TreeSelectionListener;
33import javax.swing.tree.DefaultMutableTreeNode;
34import javax.swing.tree.DefaultTreeCellRenderer;
35import javax.swing.tree.DefaultTreeModel;
36import javax.swing.tree.TreePath;
37import javax.swing.tree.TreeSelectionModel;
38
39import org.openstreetmap.josm.Main;
40import org.openstreetmap.josm.command.Command;
41import org.openstreetmap.josm.command.PseudoCommand;
42import org.openstreetmap.josm.data.osm.OsmPrimitive;
43import org.openstreetmap.josm.gui.MapFrame;
44import org.openstreetmap.josm.gui.SideButton;
45import org.openstreetmap.josm.gui.layer.OsmDataLayer;
46import org.openstreetmap.josm.gui.layer.OsmDataLayer.CommandQueueListener;
47import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
48import org.openstreetmap.josm.tools.FilteredCollection;
49import org.openstreetmap.josm.tools.GBC;
50import org.openstreetmap.josm.tools.ImageProvider;
51import org.openstreetmap.josm.tools.InputMapUtils;
52import org.openstreetmap.josm.tools.Predicate;
53import org.openstreetmap.josm.tools.Shortcut;
54
55public class CommandStackDialog extends ToggleDialog implements CommandQueueListener {
56
57    private DefaultTreeModel undoTreeModel = new DefaultTreeModel(new DefaultMutableTreeNode());
58    private DefaultTreeModel redoTreeModel = new DefaultTreeModel(new DefaultMutableTreeNode());
59
60    private JTree undoTree = new JTree(undoTreeModel);
61    private JTree redoTree = new JTree(redoTreeModel);
62
63    private UndoRedoSelectionListener undoSelectionListener;
64    private UndoRedoSelectionListener redoSelectionListener;
65
66    private JScrollPane scrollPane;
67    private JSeparator separator = new JSeparator();
68    // only visible, if separator is the top most component
69    private Component spacer = Box.createRigidArea(new Dimension(0, 3));
70
71    // last operation is remembered to select the next undo/redo entry in the list
72    // after undo/redo command
73    private UndoRedoType lastOperation = UndoRedoType.UNDO;
74
75    public CommandStackDialog(final MapFrame mapFrame) {
76        super(tr("Command Stack"), "commandstack", tr("Open a list of all commands (undo buffer)."),
77                Shortcut.registerShortcut("subwindow:commandstack", tr("Toggle: {0}",
78                tr("Command Stack")), KeyEvent.VK_O, Shortcut.ALT_SHIFT), 100, true);
79        undoTree.addMouseListener(new PopupMenuHandler());
80        undoTree.setRootVisible(false);
81        undoTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
82        undoTree.setShowsRootHandles(true);
83        undoTree.expandRow(0);
84        undoTree.setCellRenderer(new CommandCellRenderer());
85        undoSelectionListener = new UndoRedoSelectionListener(undoTree);
86        undoTree.getSelectionModel().addTreeSelectionListener(undoSelectionListener);
87        InputMapUtils.unassignCtrlShiftUpDown(undoTree, JComponent.WHEN_FOCUSED);
88       
89        redoTree.addMouseListener(new PopupMenuHandler());
90        redoTree.setRootVisible(false);
91        redoTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
92        redoTree.setShowsRootHandles(true);
93        redoTree.expandRow(0);
94        redoTree.setCellRenderer(new CommandCellRenderer());
95        redoSelectionListener = new UndoRedoSelectionListener(redoTree);
96        redoTree.getSelectionModel().addTreeSelectionListener(redoSelectionListener);
97
98        JPanel treesPanel = new JPanel(new GridBagLayout());
99
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}
Note: See TracBrowser for help on using the repository browser.