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