001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.widgets;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.GraphicsEnvironment;
007import java.awt.Toolkit;
008import java.awt.event.ActionEvent;
009import java.awt.event.KeyEvent;
010import java.beans.PropertyChangeEvent;
011import java.beans.PropertyChangeListener;
012
013import javax.swing.AbstractAction;
014import javax.swing.Action;
015import javax.swing.ImageIcon;
016import javax.swing.JMenuItem;
017import javax.swing.JPopupMenu;
018import javax.swing.KeyStroke;
019import javax.swing.event.UndoableEditEvent;
020import javax.swing.event.UndoableEditListener;
021import javax.swing.text.DefaultEditorKit;
022import javax.swing.text.JTextComponent;
023import javax.swing.undo.CannotRedoException;
024import javax.swing.undo.CannotUndoException;
025import javax.swing.undo.UndoManager;
026
027import org.openstreetmap.josm.Main;
028import org.openstreetmap.josm.tools.ImageProvider;
029
030/**
031 * A popup menu designed for text components. It displays the following actions:
032 * <ul>
033 * <li>Undo</li>
034 * <li>Redo</li>
035 * <li>Cut</li>
036 * <li>Copy</li>
037 * <li>Paste</li>
038 * <li>Delete</li>
039 * <li>Select All</li>
040 * </ul>
041 * @since 5886
042 */
043public class TextContextualPopupMenu extends JPopupMenu {
044
045    private static final String EDITABLE = "editable";
046
047    protected JTextComponent component = null;
048    protected boolean undoRedo;
049    protected final UndoAction undoAction = new UndoAction();
050    protected final RedoAction redoAction = new RedoAction();
051    protected final UndoManager undo = new UndoManager();
052
053    protected final UndoableEditListener undoEditListener = new UndoableEditListener() {
054        @Override
055        public void undoableEditHappened(UndoableEditEvent e) {
056            undo.addEdit(e.getEdit());
057            undoAction.updateUndoState();
058            redoAction.updateRedoState();
059        }
060    };
061
062    protected final PropertyChangeListener propertyChangeListener = new PropertyChangeListener() {
063        @Override
064        public void propertyChange(PropertyChangeEvent evt) {
065            if (EDITABLE.equals(evt.getPropertyName())) {
066                removeAll();
067                addMenuEntries();
068            }
069        }
070    };
071
072    /**
073     * Creates a new {@link TextContextualPopupMenu}.
074     */
075    protected TextContextualPopupMenu() {
076    }
077
078    /**
079     * Attaches this contextual menu to the given text component.
080     * A menu can only be attached to a single component.
081     * @param component The text component that will display the menu and handle its actions.
082     * @return {@code this}
083     * @see #detach()
084     */
085    protected TextContextualPopupMenu attach(JTextComponent component, boolean undoRedo) {
086        if (component != null && !isAttached()) {
087            this.component = component;
088            this.undoRedo = undoRedo;
089            if (undoRedo && component.isEditable()) {
090                component.getDocument().addUndoableEditListener(undoEditListener);
091                if (!GraphicsEnvironment.isHeadless()) {
092                    component.getInputMap().put(
093                            KeyStroke.getKeyStroke(KeyEvent.VK_Z, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()), undoAction);
094                    component.getInputMap().put(
095                            KeyStroke.getKeyStroke(KeyEvent.VK_Y, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()), redoAction);
096                }
097            }
098            addMenuEntries();
099            component.addPropertyChangeListener(EDITABLE, propertyChangeListener);
100        }
101        return this;
102    }
103
104    private void addMenuEntries() {
105        if (component.isEditable()) {
106            if (undoRedo) {
107                add(new JMenuItem(undoAction));
108                add(new JMenuItem(redoAction));
109                addSeparator();
110            }
111            addMenuEntry(component, tr("Cut"), DefaultEditorKit.cutAction, null);
112        }
113        addMenuEntry(component, tr("Copy"), DefaultEditorKit.copyAction, "copy");
114        if (component.isEditable()) {
115            addMenuEntry(component, tr("Paste"), DefaultEditorKit.pasteAction, "paste");
116            addMenuEntry(component, tr("Delete"), DefaultEditorKit.deleteNextCharAction, null);
117        }
118        addSeparator();
119        addMenuEntry(component, tr("Select All"), DefaultEditorKit.selectAllAction, null);
120    }
121
122    /**
123     * Detaches this contextual menu from its text component.
124     * @return {@code this}
125     * @see #attach(JTextComponent, boolean)
126     */
127    protected TextContextualPopupMenu detach() {
128        if (isAttached()) {
129            component.removePropertyChangeListener(EDITABLE, propertyChangeListener);
130            removeAll();
131            if (undoRedo) {
132                component.getDocument().removeUndoableEditListener(undoEditListener);
133            }
134            component = null;
135        }
136        return this;
137    }
138
139    /**
140     * Creates a new {@link TextContextualPopupMenu} and enables it for the given text component.
141     * @param component The component that will display the menu and handle its actions.
142     * @param undoRedo Enables or not Undo/Redo feature. Not recommended for table cell editors, unless each cell provides its own editor
143     * @return The {@link PopupMenuLauncher} responsible of displaying the popup menu.
144     *         Call {@link #disableMenuFor} with this object if you want to disable the menu later.
145     * @see #disableMenuFor
146     */
147    public static PopupMenuLauncher enableMenuFor(JTextComponent component, boolean undoRedo) {
148        PopupMenuLauncher launcher = new PopupMenuLauncher(new TextContextualPopupMenu().attach(component, undoRedo), true);
149        component.addMouseListener(launcher);
150        return launcher;
151    }
152
153    /**
154     * Disables the {@link TextContextualPopupMenu} attached to the given popup menu launcher and text component.
155     * @param component The component that currently displays the menu and handles its actions.
156     * @param launcher The {@link PopupMenuLauncher} obtained via {@link #enableMenuFor}.
157     * @see #enableMenuFor
158     */
159    public static void disableMenuFor(JTextComponent component, PopupMenuLauncher launcher) {
160        if (launcher.getMenu() instanceof TextContextualPopupMenu) {
161            ((TextContextualPopupMenu) launcher.getMenu()).detach();
162            component.removeMouseListener(launcher);
163        }
164    }
165
166    /**
167     * Determines if this popup is currently attached to a component.
168     * @return {@code true} if this popup is currently attached to a component, {@code false} otherwise.
169     */
170    public final boolean isAttached() {
171        return component != null;
172    }
173
174    protected void addMenuEntry(JTextComponent component,  String label, String actionName, String iconName) {
175        Action action = component.getActionMap().get(actionName);
176        if (action != null) {
177            JMenuItem mi = new JMenuItem(action);
178            mi.setText(label);
179            if (iconName != null && Main.pref.getBoolean("text.popupmenu.useicons", true)) {
180                ImageIcon icon = new ImageProvider(iconName).setWidth(16).get();
181                if (icon != null) {
182                    mi.setIcon(icon);
183                }
184            }
185            add(mi);
186        }
187    }
188
189    protected class UndoAction extends AbstractAction {
190
191        /**
192         * Constructs a new {@code UndoAction}.
193         */
194        public UndoAction() {
195            super(tr("Undo"));
196            setEnabled(false);
197        }
198
199        @Override
200        public void actionPerformed(ActionEvent e) {
201            try {
202                undo.undo();
203            } catch (CannotUndoException ex) {
204                if (Main.isTraceEnabled()) {
205                    Main.trace(ex.getMessage());
206                }
207            } finally {
208                updateUndoState();
209                redoAction.updateRedoState();
210            }
211        }
212
213        public void updateUndoState() {
214            if (undo.canUndo()) {
215                setEnabled(true);
216                putValue(Action.NAME, undo.getUndoPresentationName());
217            } else {
218                setEnabled(false);
219                putValue(Action.NAME, tr("Undo"));
220            }
221        }
222    }
223
224    protected class RedoAction extends AbstractAction {
225
226        /**
227         * Constructs a new {@code RedoAction}.
228         */
229        public RedoAction() {
230            super(tr("Redo"));
231            setEnabled(false);
232        }
233
234        @Override
235        public void actionPerformed(ActionEvent e) {
236            try {
237                undo.redo();
238            } catch (CannotRedoException ex) {
239                if (Main.isTraceEnabled()) {
240                    Main.trace(ex.getMessage());
241                }
242            } finally {
243                updateRedoState();
244                undoAction.updateUndoState();
245            }
246        }
247
248        public void updateRedoState() {
249            if (undo.canRedo()) {
250                setEnabled(true);
251                putValue(Action.NAME, undo.getRedoPresentationName());
252            } else {
253                setEnabled(false);
254                putValue(Action.NAME, tr("Redo"));
255            }
256        }
257    }
258}