// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.gui.widgets;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.awt.GraphicsEnvironment;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.beans.PropertyChangeListener;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ImageIcon;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.KeyStroke;
import javax.swing.event.UndoableEditListener;
import javax.swing.text.DefaultEditorKit;
import javax.swing.text.JTextComponent;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;
import javax.swing.undo.UndoManager;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.tools.ImageProvider;
/**
* A popup menu designed for text components. It displays the following actions:
* <ul>
* <li>Undo</li>
* <li>Redo</li>
* <li>Cut</li>
* <li>Copy</li>
* <li>Paste</li>
* <li>Delete</li>
* <li>Select All</li>
* </ul>
* @since 5886
*/
public class TextContextualPopupMenu extends JPopupMenu {
private static final String EDITABLE = "editable";
protected JTextComponent component;
protected boolean undoRedo;
protected final UndoAction undoAction = new UndoAction();
protected final RedoAction redoAction = new RedoAction();
protected final UndoManager undo = new UndoManager();
protected final transient UndoableEditListener undoEditListener = e -> {
undo.addEdit(e.getEdit());
undoAction.updateUndoState();
redoAction.updateRedoState();
};
protected final transient PropertyChangeListener propertyChangeListener = evt -> {
if (EDITABLE.equals(evt.getPropertyName())) {
removeAll();
addMenuEntries();
}
};
/**
* Creates a new {@link TextContextualPopupMenu}.
*/
protected TextContextualPopupMenu() {
// Restricts visibility
}
/**
* Attaches this contextual menu to the given text component.
* A menu can only be attached to a single component.
* @param component The text component that will display the menu and handle its actions.
* @param undoRedo {@code true} if undo/redo must be supported
* @return {@code this}
* @see #detach()
*/
protected TextContextualPopupMenu attach(JTextComponent component, boolean undoRedo) {
if (component != null && !isAttached()) {
this.component = component;
this.undoRedo = undoRedo;
if (undoRedo && component.isEditable()) {
component.getDocument().addUndoableEditListener(undoEditListener);
if (!GraphicsEnvironment.isHeadless()) {
component.getInputMap().put(
KeyStroke.getKeyStroke(KeyEvent.VK_Z, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()), undoAction);
component.getInputMap().put(
KeyStroke.getKeyStroke(KeyEvent.VK_Y, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()), redoAction);
}
}
addMenuEntries();
component.addPropertyChangeListener(EDITABLE, propertyChangeListener);
}
return this;
}
private void addMenuEntries() {
if (component.isEditable()) {
if (undoRedo) {
add(new JMenuItem(undoAction));
add(new JMenuItem(redoAction));
addSeparator();
}
addMenuEntry(component, tr("Cut"), DefaultEditorKit.cutAction, null);
}
addMenuEntry(component, tr("Copy"), DefaultEditorKit.copyAction, "copy");
if (component.isEditable()) {
addMenuEntry(component, tr("Paste"), DefaultEditorKit.pasteAction, "paste");
addMenuEntry(component, tr("Delete"), DefaultEditorKit.deleteNextCharAction, null);
}
addSeparator();
addMenuEntry(component, tr("Select All"), DefaultEditorKit.selectAllAction, null);
}
/**
* Detaches this contextual menu from its text component.
* @return {@code this}
* @see #attach(JTextComponent, boolean)
*/
protected TextContextualPopupMenu detach() {
if (isAttached()) {
component.removePropertyChangeListener(EDITABLE, propertyChangeListener);
removeAll();
if (undoRedo) {
component.getDocument().removeUndoableEditListener(undoEditListener);
}
component = null;
}
return this;
}
/**
* Creates a new {@link TextContextualPopupMenu} and enables it for the given text component.
* @param component The component that will display the menu and handle its actions.
* @param undoRedo Enables or not Undo/Redo feature. Not recommended for table cell editors, unless each cell provides its own editor
* @return The {@link PopupMenuLauncher} responsible of displaying the popup menu.
* Call {@link #disableMenuFor} with this object if you want to disable the menu later.
* @see #disableMenuFor
*/
public static PopupMenuLauncher enableMenuFor(JTextComponent component, boolean undoRedo) {
PopupMenuLauncher launcher = new PopupMenuLauncher(new TextContextualPopupMenu().attach(component, undoRedo), true);
component.addMouseListener(launcher);
return launcher;
}
/**
* Disables the {@link TextContextualPopupMenu} attached to the given popup menu launcher and text component.
* @param component The component that currently displays the menu and handles its actions.
* @param launcher The {@link PopupMenuLauncher} obtained via {@link #enableMenuFor}.
* @see #enableMenuFor
*/
public static void disableMenuFor(JTextComponent component, PopupMenuLauncher launcher) {
if (launcher.getMenu() instanceof TextContextualPopupMenu) {
((TextContextualPopupMenu) launcher.getMenu()).detach();
component.removeMouseListener(launcher);
}
}
/**
* Determines if this popup is currently attached to a component.
* @return {@code true} if this popup is currently attached to a component, {@code false} otherwise.
*/
public final boolean isAttached() {
return component != null;
}
protected void addMenuEntry(JTextComponent component, String label, String actionName, String iconName) {
Action action = component.getActionMap().get(actionName);
if (action != null) {
JMenuItem mi = new JMenuItem(action);
mi.setText(label);
if (iconName != null && Main.pref.getBoolean("text.popupmenu.useicons", true)) {
ImageIcon icon = ImageProvider.get(iconName, ImageProvider.ImageSizes.SMALLICON);
if (icon != null) {
mi.setIcon(icon);
}
}
add(mi);
}
}
protected class UndoAction extends AbstractAction {
/**
* Constructs a new {@code UndoAction}.
*/
public UndoAction() {
super(tr("Undo"));
setEnabled(false);
}
@Override
public void actionPerformed(ActionEvent e) {
try {
undo.undo();
} catch (CannotUndoException ex) {
Main.trace(ex);
} finally {
updateUndoState();
redoAction.updateRedoState();
}
}
public void updateUndoState() {
if (undo.canUndo()) {
setEnabled(true);
putValue(Action.NAME, undo.getUndoPresentationName());
} else {
setEnabled(false);
putValue(Action.NAME, tr("Undo"));
}
}
}
protected class RedoAction extends AbstractAction {
/**
* Constructs a new {@code RedoAction}.
*/
public RedoAction() {
super(tr("Redo"));
setEnabled(false);
}
@Override
public void actionPerformed(ActionEvent e) {
try {
undo.redo();
} catch (CannotRedoException ex) {
Main.trace(ex);
} finally {
updateRedoState();
undoAction.updateUndoState();
}
}
public void updateRedoState() {
if (undo.canRedo()) {
setEnabled(true);
putValue(Action.NAME, undo.getRedoPresentationName());
} else {
setEnabled(false);
putValue(Action.NAME, tr("Redo"));
}
}
}
}