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