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.event.ActionEvent; 007import java.beans.PropertyChangeEvent; 008import java.beans.PropertyChangeListener; 009 010import javax.swing.AbstractAction; 011import javax.swing.Action; 012import javax.swing.ImageIcon; 013import javax.swing.JMenuItem; 014import javax.swing.JPopupMenu; 015import javax.swing.event.UndoableEditEvent; 016import javax.swing.event.UndoableEditListener; 017import javax.swing.text.DefaultEditorKit; 018import javax.swing.text.JTextComponent; 019import javax.swing.undo.CannotUndoException; 020import javax.swing.undo.UndoManager; 021 022import org.openstreetmap.josm.Main; 023import org.openstreetmap.josm.tools.ImageProvider; 024 025/** 026 * A popup menu designed for text components. It displays the following actions: 027 * <ul> 028 * <li>Undo</li> 029 * <li>Cut</li> 030 * <li>Copy</li> 031 * <li>Paste</li> 032 * <li>Delete</li> 033 * <li>Select All</li> 034 * </ul> 035 * @since 5886 036 */ 037public class TextContextualPopupMenu extends JPopupMenu { 038 039 protected JTextComponent component = null; 040 protected UndoAction undoAction = null; 041 042 protected final PropertyChangeListener propertyChangeListener = new PropertyChangeListener() { 043 @Override public void propertyChange(PropertyChangeEvent evt) { 044 if (evt.getPropertyName().equals("editable")) { 045 removeAll(); 046 addMenuEntries(); 047 } 048 } 049 }; 050 051 /** 052 * Creates a new {@link TextContextualPopupMenu}. 053 */ 054 protected TextContextualPopupMenu() { 055 } 056 057 /** 058 * Attaches this contextual menu to the given text component. 059 * A menu can only be attached to a single component. 060 * @param component The text component that will display the menu and handle its actions. 061 * @return {@code this} 062 * @see #detach() 063 */ 064 protected TextContextualPopupMenu attach(JTextComponent component) { 065 if (component != null && !isAttached()) { 066 this.component = component; 067 if (component.isEditable()) { 068 undoAction = new UndoAction(); 069 component.getDocument().addUndoableEditListener(undoAction); 070 } 071 addMenuEntries(); 072 component.addPropertyChangeListener("editable", propertyChangeListener); 073 } 074 return this; 075 } 076 077 private void addMenuEntries() { 078 if (component.isEditable()) { 079 add(new JMenuItem(undoAction)); 080 addSeparator(); 081 addMenuEntry(component, tr("Cut"), DefaultEditorKit.cutAction, null); 082 } 083 addMenuEntry(component, tr("Copy"), DefaultEditorKit.copyAction, "copy"); 084 if (component.isEditable()) { 085 addMenuEntry(component, tr("Paste"), DefaultEditorKit.pasteAction, "paste"); 086 addMenuEntry(component, tr("Delete"), DefaultEditorKit.deleteNextCharAction, null); 087 } 088 addSeparator(); 089 addMenuEntry(component, tr("Select All"), DefaultEditorKit.selectAllAction, null); 090 } 091 092 /** 093 * Detaches this contextual menu from its text component. 094 * @return {@code this} 095 * @see #attach(JTextComponent) 096 */ 097 protected TextContextualPopupMenu detach() { 098 if (isAttached()) { 099 component.removePropertyChangeListener("editable", propertyChangeListener); 100 removeAll(); 101 if (undoAction != null) { 102 component.getDocument().removeUndoableEditListener(undoAction); 103 undoAction = null; 104 } 105 this.component = null; 106 } 107 return this; 108 } 109 110 /** 111 * Creates a new {@link TextContextualPopupMenu} and enables it for the given text component. 112 * @param component The component that will display the menu and handle its actions. 113 * @return The {@link PopupMenuLauncher} responsible of displaying the popup menu. 114 * Call {@link #disableMenuFor} with this object if you want to disable the menu later. 115 * @see #disableMenuFor(JTextComponent, PopupMenuLauncher) 116 */ 117 public static PopupMenuLauncher enableMenuFor(JTextComponent component) { 118 PopupMenuLauncher launcher = new PopupMenuLauncher(new TextContextualPopupMenu().attach(component), true); 119 component.addMouseListener(launcher); 120 return launcher; 121 } 122 123 /** 124 * Disables the {@link TextContextualPopupMenu} attached to the given popup menu launcher and text component. 125 * @param component The component that currently displays the menu and handles its actions. 126 * @param launcher The {@link PopupMenuLauncher} obtained via {@link #enableMenuFor}. 127 * @see #enableMenuFor(JTextComponent) 128 */ 129 public static void disableMenuFor(JTextComponent component, PopupMenuLauncher launcher) { 130 if (launcher.getMenu() instanceof TextContextualPopupMenu) { 131 ((TextContextualPopupMenu) launcher.getMenu()).detach(); 132 component.removeMouseListener(launcher); 133 } 134 } 135 136 /** 137 * Determines if this popup is currently attached to a component. 138 * @return {@code true} if this popup is currently attached to a component, {@code false} otherwise. 139 */ 140 public final boolean isAttached() { 141 return component != null; 142 } 143 144 protected void addMenuEntry(JTextComponent component, String label, String actionName, String iconName) { 145 Action action = component.getActionMap().get(actionName); 146 if (action != null) { 147 JMenuItem mi = new JMenuItem(action); 148 mi.setText(label); 149 if (iconName != null && Main.pref.getBoolean("text.popupmenu.useicons", true)) { 150 ImageIcon icon = new ImageProvider(iconName).setWidth(16).get(); 151 if (icon != null) { 152 mi.setIcon(icon); 153 } 154 } 155 add(mi); 156 } 157 } 158 159 protected class UndoAction extends AbstractAction implements UndoableEditListener { 160 161 private final UndoManager undoManager = new UndoManager(); 162 163 public UndoAction() { 164 super(tr("Undo")); 165 setEnabled(false); 166 } 167 168 @Override 169 public void undoableEditHappened(UndoableEditEvent e) { 170 undoManager.addEdit(e.getEdit()); 171 setEnabled(undoManager.canUndo()); 172 } 173 174 @Override 175 public void actionPerformed(ActionEvent e) { 176 try { 177 undoManager.undo(); 178 } catch (CannotUndoException ex) { 179 // Ignored 180 } finally { 181 setEnabled(undoManager.canUndo()); 182 } 183 } 184 } 185}