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