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.event.ActionEvent;
007import java.beans.PropertyChangeEvent;
008import java.beans.PropertyChangeListener;
009
010import javax.swing.AbstractAction;
011import javax.swing.Action;
012import javax.swing.ImageIcon;
013import javax.swing.JMenuItem;
014import javax.swing.JPopupMenu;
015import javax.swing.event.UndoableEditEvent;
016import javax.swing.event.UndoableEditListener;
017import javax.swing.text.DefaultEditorKit;
018import javax.swing.text.JTextComponent;
019import javax.swing.undo.CannotUndoException;
020import javax.swing.undo.UndoManager;
021
022import org.openstreetmap.josm.Main;
023import org.openstreetmap.josm.tools.ImageProvider;
024
025/**
026 * A popup menu designed for text components. It displays the following actions:
027 * <ul>
028 * <li>Undo</li>
029 * <li>Cut</li>
030 * <li>Copy</li>
031 * <li>Paste</li>
032 * <li>Delete</li>
033 * <li>Select All</li>
034 * </ul>
035 * @since 5886
036 */
037public class TextContextualPopupMenu extends JPopupMenu {
038
039    protected JTextComponent component = null;
040    protected UndoAction undoAction = null;
041
042    protected final PropertyChangeListener propertyChangeListener = new PropertyChangeListener() {
043        @Override public void propertyChange(PropertyChangeEvent evt) {
044            if ("editable".equals(evt.getPropertyName())) {
045                removeAll();
046                addMenuEntries();
047            }
048        }
049    };
050
051    /**
052     * Creates a new {@link TextContextualPopupMenu}.
053     */
054    protected TextContextualPopupMenu() {
055    }
056
057    /**
058     * Attaches this contextual menu to the given text component.
059     * A menu can only be attached to a single component.
060     * @param component The text component that will display the menu and handle its actions.
061     * @return {@code this}
062     * @see #detach()
063     */
064    protected TextContextualPopupMenu attach(JTextComponent component) {
065        if (component != null && !isAttached()) {
066            this.component = component;
067            if (component.isEditable()) {
068                undoAction = new UndoAction();
069                component.getDocument().addUndoableEditListener(undoAction);
070            }
071            addMenuEntries();
072            component.addPropertyChangeListener("editable", propertyChangeListener);
073        }
074        return this;
075    }
076
077    private void addMenuEntries() {
078        if (component.isEditable()) {
079            add(new JMenuItem(undoAction));
080            addSeparator();
081            addMenuEntry(component, tr("Cut"), DefaultEditorKit.cutAction, null);
082        }
083        addMenuEntry(component, tr("Copy"), DefaultEditorKit.copyAction, "copy");
084        if (component.isEditable()) {
085            addMenuEntry(component, tr("Paste"), DefaultEditorKit.pasteAction, "paste");
086            addMenuEntry(component, tr("Delete"), DefaultEditorKit.deleteNextCharAction, null);
087        }
088        addSeparator();
089        addMenuEntry(component, tr("Select All"), DefaultEditorKit.selectAllAction, null);
090    }
091
092    /**
093     * Detaches this contextual menu from its text component.
094     * @return {@code this}
095     * @see #attach(JTextComponent)
096     */
097    protected TextContextualPopupMenu detach() {
098        if (isAttached()) {
099            component.removePropertyChangeListener("editable", propertyChangeListener);
100            removeAll();
101            if (undoAction != null) {
102                component.getDocument().removeUndoableEditListener(undoAction);
103                undoAction = null;
104            }
105            this.component = null;
106        }
107        return this;
108    }
109
110    /**
111     * Creates a new {@link TextContextualPopupMenu} and enables it for the given text component.
112     * @param component The component that will display the menu and handle its actions.
113     * @return The {@link PopupMenuLauncher} responsible of displaying the popup menu.
114     *         Call {@link #disableMenuFor} with this object if you want to disable the menu later.
115     * @see #disableMenuFor(JTextComponent, PopupMenuLauncher)
116     */
117    public static PopupMenuLauncher enableMenuFor(JTextComponent component) {
118        PopupMenuLauncher launcher = new PopupMenuLauncher(new TextContextualPopupMenu().attach(component), true);
119        component.addMouseListener(launcher);
120        return launcher;
121    }
122
123    /**
124     * Disables the {@link TextContextualPopupMenu} attached to the given popup menu launcher and text component.
125     * @param component The component that currently displays the menu and handles its actions.
126     * @param launcher The {@link PopupMenuLauncher} obtained via {@link #enableMenuFor}.
127     * @see #enableMenuFor(JTextComponent)
128     */
129    public static void disableMenuFor(JTextComponent component, PopupMenuLauncher launcher) {
130        if (launcher.getMenu() instanceof TextContextualPopupMenu) {
131            ((TextContextualPopupMenu) launcher.getMenu()).detach();
132            component.removeMouseListener(launcher);
133        }
134    }
135
136    /**
137     * Determines if this popup is currently attached to a component.
138     * @return {@code true} if this popup is currently attached to a component, {@code false} otherwise.
139     */
140    public final boolean isAttached() {
141        return component != null;
142    }
143
144    protected void addMenuEntry(JTextComponent component,  String label, String actionName, String iconName) {
145        Action action = component.getActionMap().get(actionName);
146        if (action != null) {
147            JMenuItem mi = new JMenuItem(action);
148            mi.setText(label);
149            if (iconName != null && Main.pref.getBoolean("text.popupmenu.useicons", true)) {
150                ImageIcon icon = new ImageProvider(iconName).setWidth(16).get();
151                if (icon != null) {
152                    mi.setIcon(icon);
153                }
154            }
155            add(mi);
156        }
157    }
158
159    protected static class UndoAction extends AbstractAction implements UndoableEditListener {
160
161        private final UndoManager undoManager = new UndoManager();
162
163        public UndoAction() {
164            super(tr("Undo"));
165            setEnabled(false);
166        }
167
168        @Override
169        public void undoableEditHappened(UndoableEditEvent e) {
170            undoManager.addEdit(e.getEdit());
171            setEnabled(undoManager.canUndo());
172        }
173
174        @Override
175        public void actionPerformed(ActionEvent e) {
176            try {
177                undoManager.undo();
178            } catch (CannotUndoException ex) {
179                // Ignored
180            } finally {
181                setEnabled(undoManager.canUndo());
182            }
183        }
184    }
185}