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