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}