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; 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 transient 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 transient 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 // Restricts visibility 077 } 078 079 /** 080 * Attaches this contextual menu to the given text component. 081 * A menu can only be attached to a single component. 082 * @param component The text component that will display the menu and handle its actions. 083 * @param undoRedo {@code true} if undo/redo must be supported 084 * @return {@code this} 085 * @see #detach() 086 */ 087 protected TextContextualPopupMenu attach(JTextComponent component, boolean undoRedo) { 088 if (component != null && !isAttached()) { 089 this.component = component; 090 this.undoRedo = undoRedo; 091 if (undoRedo && component.isEditable()) { 092 component.getDocument().addUndoableEditListener(undoEditListener); 093 if (!GraphicsEnvironment.isHeadless()) { 094 component.getInputMap().put( 095 KeyStroke.getKeyStroke(KeyEvent.VK_Z, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()), undoAction); 096 component.getInputMap().put( 097 KeyStroke.getKeyStroke(KeyEvent.VK_Y, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()), redoAction); 098 } 099 } 100 addMenuEntries(); 101 component.addPropertyChangeListener(EDITABLE, propertyChangeListener); 102 } 103 return this; 104 } 105 106 private void addMenuEntries() { 107 if (component.isEditable()) { 108 if (undoRedo) { 109 add(new JMenuItem(undoAction)); 110 add(new JMenuItem(redoAction)); 111 addSeparator(); 112 } 113 addMenuEntry(component, tr("Cut"), DefaultEditorKit.cutAction, null); 114 } 115 addMenuEntry(component, tr("Copy"), DefaultEditorKit.copyAction, "copy"); 116 if (component.isEditable()) { 117 addMenuEntry(component, tr("Paste"), DefaultEditorKit.pasteAction, "paste"); 118 addMenuEntry(component, tr("Delete"), DefaultEditorKit.deleteNextCharAction, null); 119 } 120 addSeparator(); 121 addMenuEntry(component, tr("Select All"), DefaultEditorKit.selectAllAction, null); 122 } 123 124 /** 125 * Detaches this contextual menu from its text component. 126 * @return {@code this} 127 * @see #attach(JTextComponent, boolean) 128 */ 129 protected TextContextualPopupMenu detach() { 130 if (isAttached()) { 131 component.removePropertyChangeListener(EDITABLE, propertyChangeListener); 132 removeAll(); 133 if (undoRedo) { 134 component.getDocument().removeUndoableEditListener(undoEditListener); 135 } 136 component = null; 137 } 138 return this; 139 } 140 141 /** 142 * Creates a new {@link TextContextualPopupMenu} and enables it for the given text component. 143 * @param component The component that will display the menu and handle its actions. 144 * @param undoRedo Enables or not Undo/Redo feature. Not recommended for table cell editors, unless each cell provides its own editor 145 * @return The {@link PopupMenuLauncher} responsible of displaying the popup menu. 146 * Call {@link #disableMenuFor} with this object if you want to disable the menu later. 147 * @see #disableMenuFor 148 */ 149 public static PopupMenuLauncher enableMenuFor(JTextComponent component, boolean undoRedo) { 150 PopupMenuLauncher launcher = new PopupMenuLauncher(new TextContextualPopupMenu().attach(component, undoRedo), true); 151 component.addMouseListener(launcher); 152 return launcher; 153 } 154 155 /** 156 * Disables the {@link TextContextualPopupMenu} attached to the given popup menu launcher and text component. 157 * @param component The component that currently displays the menu and handles its actions. 158 * @param launcher The {@link PopupMenuLauncher} obtained via {@link #enableMenuFor}. 159 * @see #enableMenuFor 160 */ 161 public static void disableMenuFor(JTextComponent component, PopupMenuLauncher launcher) { 162 if (launcher.getMenu() instanceof TextContextualPopupMenu) { 163 ((TextContextualPopupMenu) launcher.getMenu()).detach(); 164 component.removeMouseListener(launcher); 165 } 166 } 167 168 /** 169 * Determines if this popup is currently attached to a component. 170 * @return {@code true} if this popup is currently attached to a component, {@code false} otherwise. 171 */ 172 public final boolean isAttached() { 173 return component != null; 174 } 175 176 protected void addMenuEntry(JTextComponent component, String label, String actionName, String iconName) { 177 Action action = component.getActionMap().get(actionName); 178 if (action != null) { 179 JMenuItem mi = new JMenuItem(action); 180 mi.setText(label); 181 if (iconName != null && Main.pref.getBoolean("text.popupmenu.useicons", true)) { 182 ImageIcon icon = new ImageProvider(iconName).setWidth(16).get(); 183 if (icon != null) { 184 mi.setIcon(icon); 185 } 186 } 187 add(mi); 188 } 189 } 190 191 protected class UndoAction extends AbstractAction { 192 193 /** 194 * Constructs a new {@code UndoAction}. 195 */ 196 public UndoAction() { 197 super(tr("Undo")); 198 setEnabled(false); 199 } 200 201 @Override 202 public void actionPerformed(ActionEvent e) { 203 try { 204 undo.undo(); 205 } catch (CannotUndoException ex) { 206 if (Main.isTraceEnabled()) { 207 Main.trace(ex.getMessage()); 208 } 209 } finally { 210 updateUndoState(); 211 redoAction.updateRedoState(); 212 } 213 } 214 215 public void updateUndoState() { 216 if (undo.canUndo()) { 217 setEnabled(true); 218 putValue(Action.NAME, undo.getUndoPresentationName()); 219 } else { 220 setEnabled(false); 221 putValue(Action.NAME, tr("Undo")); 222 } 223 } 224 } 225 226 protected class RedoAction extends AbstractAction { 227 228 /** 229 * Constructs a new {@code RedoAction}. 230 */ 231 public RedoAction() { 232 super(tr("Redo")); 233 setEnabled(false); 234 } 235 236 @Override 237 public void actionPerformed(ActionEvent e) { 238 try { 239 undo.redo(); 240 } catch (CannotRedoException ex) { 241 if (Main.isTraceEnabled()) { 242 Main.trace(ex.getMessage()); 243 } 244 } finally { 245 updateRedoState(); 246 undoAction.updateUndoState(); 247 } 248 } 249 250 public void updateRedoState() { 251 if (undo.canRedo()) { 252 setEnabled(true); 253 putValue(Action.NAME, undo.getRedoPresentationName()); 254 } else { 255 setEnabled(false); 256 putValue(Action.NAME, tr("Redo")); 257 } 258 } 259 } 260}