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

Last change on this file since 12718 was 12718, checked in by Don-vip, 3 months ago

see #13036 - see #15229 - see #15182 - make Commands depends only on a DataSet, not a Layer. This removes a lot of GUI dependencies

  • Property svn:eol-style set to native
File size: 18.3 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.dialogs;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.Component;
7import java.awt.Dimension;
8import java.awt.GridBagLayout;
9import java.awt.event.ActionEvent;
10import java.awt.event.KeyEvent;
11import java.awt.event.MouseEvent;
12import java.util.ArrayList;
13import java.util.Arrays;
14import java.util.Collection;
15import java.util.LinkedHashSet;
16import java.util.List;
17import java.util.Set;
18
19import javax.swing.AbstractAction;
20import javax.swing.Box;
21import javax.swing.JComponent;
22import javax.swing.JLabel;
23import javax.swing.JPanel;
24import javax.swing.JPopupMenu;
25import javax.swing.JScrollPane;
26import javax.swing.JSeparator;
27import javax.swing.JTree;
28import javax.swing.event.TreeModelEvent;
29import javax.swing.event.TreeModelListener;
30import javax.swing.event.TreeSelectionEvent;
31import javax.swing.event.TreeSelectionListener;
32import javax.swing.tree.DefaultMutableTreeNode;
33import javax.swing.tree.DefaultTreeCellRenderer;
34import javax.swing.tree.DefaultTreeModel;
35import javax.swing.tree.TreePath;
36import javax.swing.tree.TreeSelectionModel;
37
38import org.openstreetmap.josm.actions.AutoScaleAction;
39import org.openstreetmap.josm.command.Command;
40import org.openstreetmap.josm.command.PseudoCommand;
41import org.openstreetmap.josm.data.UndoRedoHandler.CommandQueueListener;
42import org.openstreetmap.josm.data.osm.DataSet;
43import org.openstreetmap.josm.data.osm.OsmPrimitive;
44import org.openstreetmap.josm.gui.MainApplication;
45import org.openstreetmap.josm.gui.SideButton;
46import org.openstreetmap.josm.gui.layer.OsmDataLayer;
47import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
48import org.openstreetmap.josm.tools.GBC;
49import org.openstreetmap.josm.tools.ImageProvider;
50import org.openstreetmap.josm.tools.InputMapUtils;
51import org.openstreetmap.josm.tools.Shortcut;
52import org.openstreetmap.josm.tools.SubclassFilteredCollection;
53
54/**
55 * Dialog displaying list of all executed commands (undo/redo buffer).
56 * @since 94
57 */
58public class CommandStackDialog extends ToggleDialog implements CommandQueueListener {
59
60    private final DefaultTreeModel undoTreeModel = new DefaultTreeModel(new DefaultMutableTreeNode());
61    private final DefaultTreeModel redoTreeModel = new DefaultTreeModel(new DefaultMutableTreeNode());
62
63    private final JTree undoTree = new JTree(undoTreeModel);
64    private final JTree redoTree = new JTree(redoTreeModel);
65
66    private final transient UndoRedoSelectionListener undoSelectionListener;
67    private final transient UndoRedoSelectionListener redoSelectionListener;
68
69    private final JScrollPane scrollPane;
70    private final JSeparator separator = new JSeparator();
71    // only visible, if separator is the top most component
72    private final Component spacer = Box.createRigidArea(new Dimension(0, 3));
73
74    // last operation is remembered to select the next undo/redo entry in the list
75    // after undo/redo command
76    private UndoRedoType lastOperation = UndoRedoType.UNDO;
77
78    // Actions for context menu and Enter key
79    private final SelectAction selectAction = new SelectAction();
80    private final SelectAndZoomAction selectAndZoomAction = new SelectAndZoomAction();
81
82    /**
83     * Constructs a new {@code CommandStackDialog}.
84     */
85    public CommandStackDialog() {
86        super(tr("Command Stack"), "commandstack", tr("Open a list of all commands (undo buffer)."),
87                Shortcut.registerShortcut("subwindow:commandstack", tr("Toggle: {0}",
88                tr("Command Stack")), KeyEvent.VK_O, Shortcut.ALT_SHIFT), 100);
89        undoTree.addMouseListener(new MouseEventHandler());
90        undoTree.setRootVisible(false);
91        undoTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
92        undoTree.setShowsRootHandles(true);
93        undoTree.expandRow(0);
94        undoTree.setCellRenderer(new CommandCellRenderer());
95        undoSelectionListener = new UndoRedoSelectionListener(undoTree);
96        undoTree.getSelectionModel().addTreeSelectionListener(undoSelectionListener);
97        InputMapUtils.unassignCtrlShiftUpDown(undoTree, JComponent.WHEN_FOCUSED);
98
99        redoTree.addMouseListener(new MouseEventHandler());
100        redoTree.setRootVisible(false);
101        redoTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
102        redoTree.setShowsRootHandles(true);
103        redoTree.expandRow(0);
104        redoTree.setCellRenderer(new CommandCellRenderer());
105        redoSelectionListener = new UndoRedoSelectionListener(redoTree);
106        redoTree.getSelectionModel().addTreeSelectionListener(redoSelectionListener);
107        InputMapUtils.unassignCtrlShiftUpDown(redoTree, JComponent.WHEN_FOCUSED);
108
109        JPanel treesPanel = new JPanel(new GridBagLayout());
110
111        treesPanel.add(spacer, GBC.eol());
112        spacer.setVisible(false);
113        treesPanel.add(undoTree, GBC.eol().fill(GBC.HORIZONTAL));
114        separator.setVisible(false);
115        treesPanel.add(separator, GBC.eol().fill(GBC.HORIZONTAL));
116        treesPanel.add(redoTree, GBC.eol().fill(GBC.HORIZONTAL));
117        treesPanel.add(Box.createRigidArea(new Dimension(0, 0)), GBC.std().weight(0, 1));
118        treesPanel.setBackground(redoTree.getBackground());
119
120        wireUpdateEnabledStateUpdater(selectAction, undoTree);
121        wireUpdateEnabledStateUpdater(selectAction, redoTree);
122
123        UndoRedoAction undoAction = new UndoRedoAction(UndoRedoType.UNDO);
124        wireUpdateEnabledStateUpdater(undoAction, undoTree);
125
126        UndoRedoAction redoAction = new UndoRedoAction(UndoRedoType.REDO);
127        wireUpdateEnabledStateUpdater(redoAction, redoTree);
128
129        scrollPane = (JScrollPane) createLayout(treesPanel, true, Arrays.asList(
130            new SideButton(selectAction),
131            new SideButton(undoAction),
132            new SideButton(redoAction)
133        ));
134
135        InputMapUtils.addEnterAction(undoTree, selectAndZoomAction);
136        InputMapUtils.addEnterAction(redoTree, selectAndZoomAction);
137    }
138
139    private static class CommandCellRenderer extends DefaultTreeCellRenderer {
140        @Override
141        public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row,
142                boolean hasFocus) {
143            super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
144            DefaultMutableTreeNode v = (DefaultMutableTreeNode) value;
145            if (v.getUserObject() instanceof JLabel) {
146                JLabel l = (JLabel) v.getUserObject();
147                setIcon(l.getIcon());
148                setText(l.getText());
149            }
150            return this;
151        }
152    }
153
154    private void updateTitle() {
155        int undo = undoTreeModel.getChildCount(undoTreeModel.getRoot());
156        int redo = redoTreeModel.getChildCount(redoTreeModel.getRoot());
157        if (undo > 0 || redo > 0) {
158            setTitle(tr("Command Stack: Undo: {0} / Redo: {1}", undo, redo));
159        } else {
160            setTitle(tr("Command Stack"));
161        }
162    }
163
164    /**
165     * Selection listener for undo and redo area.
166     * If one is clicked, takes away the selection from the other, so
167     * it behaves as if it was one component.
168     */
169    private class UndoRedoSelectionListener implements TreeSelectionListener {
170        private final JTree source;
171
172        UndoRedoSelectionListener(JTree source) {
173            this.source = source;
174        }
175
176        @Override
177        public void valueChanged(TreeSelectionEvent e) {
178            if (source == undoTree) {
179                redoTree.getSelectionModel().removeTreeSelectionListener(redoSelectionListener);
180                redoTree.clearSelection();
181                redoTree.getSelectionModel().addTreeSelectionListener(redoSelectionListener);
182            }
183            if (source == redoTree) {
184                undoTree.getSelectionModel().removeTreeSelectionListener(undoSelectionListener);
185                undoTree.clearSelection();
186                undoTree.getSelectionModel().addTreeSelectionListener(undoSelectionListener);
187            }
188        }
189    }
190
191    /**
192     * Wires updater for enabled state to the events. Also updates dialog title if needed.
193     * @param updater updater
194     * @param tree tree on which wire updater
195     */
196    protected void wireUpdateEnabledStateUpdater(final IEnabledStateUpdating updater, JTree tree) {
197        addShowNotifyListener(updater);
198
199        tree.addTreeSelectionListener(e -> updater.updateEnabledState());
200
201        tree.getModel().addTreeModelListener(new TreeModelListener() {
202            @Override
203            public void treeNodesChanged(TreeModelEvent e) {
204                updater.updateEnabledState();
205                updateTitle();
206            }
207
208            @Override
209            public void treeNodesInserted(TreeModelEvent e) {
210                updater.updateEnabledState();
211                updateTitle();
212            }
213
214            @Override
215            public void treeNodesRemoved(TreeModelEvent e) {
216                updater.updateEnabledState();
217                updateTitle();
218            }
219
220            @Override
221            public void treeStructureChanged(TreeModelEvent e) {
222                updater.updateEnabledState();
223                updateTitle();
224            }
225        });
226    }
227
228    @Override
229    public void showNotify() {
230        buildTrees();
231        for (IEnabledStateUpdating listener : showNotifyListener) {
232            listener.updateEnabledState();
233        }
234        MainApplication.undoRedo.addCommandQueueListener(this);
235    }
236
237    /**
238     * Simple listener setup to update the button enabled state when the side dialog shows.
239     */
240    private final transient Set<IEnabledStateUpdating> showNotifyListener = new LinkedHashSet<>();
241
242    private void addShowNotifyListener(IEnabledStateUpdating listener) {
243        showNotifyListener.add(listener);
244    }
245
246    @Override
247    public void hideNotify() {
248        undoTreeModel.setRoot(new DefaultMutableTreeNode());
249        redoTreeModel.setRoot(new DefaultMutableTreeNode());
250        MainApplication.undoRedo.removeCommandQueueListener(this);
251    }
252
253    /**
254     * Build the trees of undo and redo commands (initially or when
255     * they have changed).
256     */
257    private void buildTrees() {
258        setTitle(tr("Command Stack"));
259        if (MainApplication.getLayerManager().getEditLayer() == null)
260            return;
261
262        List<Command> undoCommands = MainApplication.undoRedo.commands;
263        DefaultMutableTreeNode undoRoot = new DefaultMutableTreeNode();
264        for (int i = 0; i < undoCommands.size(); ++i) {
265            undoRoot.add(getNodeForCommand(undoCommands.get(i), i));
266        }
267        undoTreeModel.setRoot(undoRoot);
268
269        List<Command> redoCommands = MainApplication.undoRedo.redoCommands;
270        DefaultMutableTreeNode redoRoot = new DefaultMutableTreeNode();
271        for (int i = 0; i < redoCommands.size(); ++i) {
272            redoRoot.add(getNodeForCommand(redoCommands.get(i), i));
273        }
274        redoTreeModel.setRoot(redoRoot);
275        if (redoTreeModel.getChildCount(redoRoot) > 0) {
276            redoTree.scrollRowToVisible(0);
277            scrollPane.getHorizontalScrollBar().setValue(0);
278        }
279
280        separator.setVisible(!undoCommands.isEmpty() || !redoCommands.isEmpty());
281        spacer.setVisible(undoCommands.isEmpty() && !redoCommands.isEmpty());
282
283        // if one tree is empty, move selection to the other
284        switch (lastOperation) {
285        case UNDO:
286            if (undoCommands.isEmpty()) {
287                lastOperation = UndoRedoType.REDO;
288            }
289            break;
290        case REDO:
291            if (redoCommands.isEmpty()) {
292                lastOperation = UndoRedoType.UNDO;
293            }
294            break;
295        }
296
297        // select the next command to undo/redo
298        switch (lastOperation) {
299        case UNDO:
300            undoTree.setSelectionRow(undoTree.getRowCount()-1);
301            break;
302        case REDO:
303            redoTree.setSelectionRow(0);
304            break;
305        }
306
307        undoTree.scrollRowToVisible(undoTreeModel.getChildCount(undoRoot)-1);
308        scrollPane.getHorizontalScrollBar().setValue(0);
309    }
310
311    /**
312     * Wraps a command in a CommandListMutableTreeNode.
313     * Recursively adds child commands.
314     * @param c the command
315     * @param idx index
316     * @return the resulting node
317     */
318    protected CommandListMutableTreeNode getNodeForCommand(PseudoCommand c, int idx) {
319        CommandListMutableTreeNode node = new CommandListMutableTreeNode(c, idx);
320        if (c.getChildren() != null) {
321            List<PseudoCommand> children = new ArrayList<>(c.getChildren());
322            for (int i = 0; i < children.size(); ++i) {
323                node.add(getNodeForCommand(children.get(i), i));
324            }
325        }
326        return node;
327    }
328
329    /**
330     * Return primitives that are affected by some command
331     * @param path GUI elements
332     * @return collection of affected primitives, onluy usable ones
333     */
334    protected static Collection<? extends OsmPrimitive> getAffectedPrimitives(TreePath path) {
335        PseudoCommand c = ((CommandListMutableTreeNode) path.getLastPathComponent()).getCommand();
336        final OsmDataLayer currentLayer = MainApplication.getLayerManager().getEditLayer();
337        return new SubclassFilteredCollection<>(
338                c.getParticipatingPrimitives(),
339                o -> {
340                    OsmPrimitive p = currentLayer.data.getPrimitiveById(o);
341                    return p != null && p.isUsable();
342                }
343        );
344    }
345
346    @Override
347    public void commandChanged(int queueSize, int redoSize) {
348        if (!isVisible())
349            return;
350        buildTrees();
351    }
352
353    /**
354     * Action that selects the objects that take part in a command.
355     */
356    public class SelectAction extends AbstractAction implements IEnabledStateUpdating {
357
358        /**
359         * Constructs a new {@code SelectAction}.
360         */
361        public SelectAction() {
362            putValue(NAME, tr("Select"));
363            putValue(SHORT_DESCRIPTION, tr("Selects the objects that take part in this command (unless currently deleted)"));
364            new ImageProvider("dialogs", "select").getResource().attachImageIcon(this, true);
365        }
366
367        @Override
368        public void actionPerformed(ActionEvent e) {
369            TreePath path;
370            if (!undoTree.isSelectionEmpty()) {
371                path = undoTree.getSelectionPath();
372            } else if (!redoTree.isSelectionEmpty()) {
373                path = redoTree.getSelectionPath();
374            } else
375                throw new IllegalStateException();
376
377            DataSet dataSet = MainApplication.getLayerManager().getEditDataSet();
378            if (dataSet == null) return;
379            dataSet.setSelected(getAffectedPrimitives(path));
380        }
381
382        @Override
383        public void updateEnabledState() {
384            setEnabled(!undoTree.isSelectionEmpty() || !redoTree.isSelectionEmpty());
385        }
386    }
387
388    /**
389     * Action that selects the objects that take part in a command, then zoom to them.
390     */
391    public class SelectAndZoomAction extends SelectAction {
392        /**
393         * Constructs a new {@code SelectAndZoomAction}.
394         */
395        public SelectAndZoomAction() {
396            putValue(NAME, tr("Select and zoom"));
397            putValue(SHORT_DESCRIPTION,
398                    tr("Selects the objects that take part in this command (unless currently deleted), then and zooms to it"));
399            new ImageProvider("dialogs/autoscale", "selection").getResource().attachImageIcon(this, true);
400        }
401
402        @Override
403        public void actionPerformed(ActionEvent e) {
404            super.actionPerformed(e);
405            AutoScaleAction.autoScale("selection");
406        }
407    }
408
409    /**
410     * undo / redo switch to reduce duplicate code
411     */
412    protected enum UndoRedoType {
413        UNDO,
414        REDO
415    }
416
417    /**
418     * Action to undo or redo all commands up to (and including) the seleced item.
419     */
420    protected class UndoRedoAction extends AbstractAction implements IEnabledStateUpdating {
421        private final UndoRedoType type;
422        private final JTree tree;
423
424        /**
425         * constructor
426         * @param type decide whether it is an undo action or a redo action
427         */
428        public UndoRedoAction(UndoRedoType type) {
429            this.type = type;
430            if (UndoRedoType.UNDO == type) {
431                tree = undoTree;
432                putValue(NAME, tr("Undo"));
433                putValue(SHORT_DESCRIPTION, tr("Undo the selected and all later commands"));
434                new ImageProvider("undo").getResource().attachImageIcon(this, true);
435            } else {
436                tree = redoTree;
437                putValue(NAME, tr("Redo"));
438                putValue(SHORT_DESCRIPTION, tr("Redo the selected and all earlier commands"));
439                new ImageProvider("redo").getResource().attachImageIcon(this, true);
440            }
441        }
442
443        @Override
444        public void actionPerformed(ActionEvent e) {
445            lastOperation = type;
446            TreePath path = tree.getSelectionPath();
447
448            // we can only undo top level commands
449            if (path.getPathCount() != 2)
450                throw new IllegalStateException();
451
452            int idx = ((CommandListMutableTreeNode) path.getLastPathComponent()).getIndex();
453
454            // calculate the number of commands to undo/redo; then do it
455            switch (type) {
456            case UNDO:
457                int numUndo = ((DefaultMutableTreeNode) undoTreeModel.getRoot()).getChildCount() - idx;
458                MainApplication.undoRedo.undo(numUndo);
459                break;
460            case REDO:
461                int numRedo = idx+1;
462                MainApplication.undoRedo.redo(numRedo);
463                break;
464            }
465            MainApplication.getMap().repaint();
466        }
467
468        @Override
469        public void updateEnabledState() {
470            // do not allow execution if nothing is selected or a sub command was selected
471            setEnabled(!tree.isSelectionEmpty() && tree.getSelectionPath().getPathCount() == 2);
472        }
473    }
474
475    class MouseEventHandler extends PopupMenuLauncher {
476
477        MouseEventHandler() {
478            super(new CommandStackPopup());
479        }
480
481        @Override
482        public void mouseClicked(MouseEvent evt) {
483            if (isDoubleClick(evt)) {
484                selectAndZoomAction.actionPerformed(null);
485            }
486        }
487    }
488
489    private class CommandStackPopup extends JPopupMenu {
490        CommandStackPopup() {
491            add(selectAction);
492            add(selectAndZoomAction);
493        }
494    }
495}
Note: See TracBrowser for help on using the repository browser.