source: josm/trunk/src/org/openstreetmap/josm/gui/widgets/TextContextualPopupMenu.java@ 16109

Last change on this file since 16109 was 14977, checked in by Don-vip, 5 years ago

ensures consistency of upload comment:

  • fix #11168 - ctrl-z/undo could reset unwanted old changeset comment
  • fix #13474 - selecting "new changeset" after having entered a changeset comment did reset it to the previous value
  • fix #17452 - ctrl-enter while typing a changeset comment did upload with the previous value
  • fix behaviour of upload.comment.max-age: values were reset after 5 months instead of intended 4 hours because seconds were compared to milliseconds
  • avoid creation of unneeded undo/redo internal classes for non-editable text fields
  • ensures consistency of upload dialog if upload.comment properties are modified manually from advanced preferences
  • add a source attribute to preference events to know which class modified the preference entry
  • refactor reflection utils
  • Property svn:eol-style set to native
File size: 9.9 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.widgets;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.GraphicsEnvironment;
7import java.awt.event.ActionEvent;
8import java.awt.event.KeyEvent;
9import java.beans.PropertyChangeListener;
10import java.util.HashMap;
11import java.util.Map;
12
13import javax.swing.AbstractAction;
14import javax.swing.Action;
15import javax.swing.ImageIcon;
16import javax.swing.JMenuItem;
17import javax.swing.JPopupMenu;
18import javax.swing.KeyStroke;
19import javax.swing.event.UndoableEditListener;
20import javax.swing.text.DefaultEditorKit;
21import javax.swing.text.JTextComponent;
22import javax.swing.undo.CannotRedoException;
23import javax.swing.undo.CannotUndoException;
24import javax.swing.undo.UndoManager;
25
26import org.openstreetmap.josm.spi.preferences.Config;
27import org.openstreetmap.josm.tools.ImageProvider;
28import org.openstreetmap.josm.tools.Logging;
29import org.openstreetmap.josm.tools.PlatformManager;
30
31/**
32 * A popup menu designed for text components. It displays the following actions:
33 * <ul>
34 * <li>Undo</li>
35 * <li>Redo</li>
36 * <li>Cut</li>
37 * <li>Copy</li>
38 * <li>Paste</li>
39 * <li>Delete</li>
40 * <li>Select All</li>
41 * </ul>
42 * @since 5886
43 */
44public class TextContextualPopupMenu extends JPopupMenu {
45
46 private static final String EDITABLE = "editable";
47
48 private static final Map<String, ImageIcon> iconCache = new HashMap<>();
49
50 private static ImageIcon loadIcon(String iconName) {
51 return iconCache.computeIfAbsent(iconName,
52 x -> new ImageProvider(x).setOptional(true).setSize(ImageProvider.ImageSizes.SMALLICON).get());
53 }
54
55 protected JTextComponent component;
56 protected boolean undoRedo;
57 protected final UndoAction undoAction = new UndoAction();
58 protected final RedoAction redoAction = new RedoAction();
59 protected final UndoManager undo = new UndoManager();
60
61 protected final transient UndoableEditListener undoEditListener = e -> {
62 undo.addEdit(e.getEdit());
63 updateUndoRedoState();
64 };
65
66 protected final transient PropertyChangeListener propertyChangeListener = evt -> {
67 if (EDITABLE.equals(evt.getPropertyName())) {
68 removeAll();
69 addMenuEntries();
70 }
71 };
72
73 /**
74 * Creates a new {@link TextContextualPopupMenu}.
75 */
76 protected TextContextualPopupMenu() {
77 // Restricts visibility
78 }
79
80 private void updateUndoRedoState() {
81 undoAction.updateUndoState();
82 redoAction.updateRedoState();
83 }
84
85 /**
86 * Attaches this contextual menu to the given text component.
87 * A menu can only be attached to a single component.
88 * @param component The text component that will display the menu and handle its actions.
89 * @param undoRedo {@code true} if undo/redo must be supported
90 * @return {@code this}
91 * @see #detach()
92 */
93 protected TextContextualPopupMenu attach(JTextComponent component, boolean undoRedo) {
94 if (component != null && !isAttached()) {
95 this.component = component;
96 if (undoRedo && component.isEditable()) {
97 enableUndoRedo();
98 }
99 addMenuEntries();
100 component.addPropertyChangeListener(EDITABLE, propertyChangeListener);
101 }
102 return this;
103 }
104
105 private void enableUndoRedo() {
106 if (!undoRedo) {
107 component.getDocument().addUndoableEditListener(undoEditListener);
108 if (!GraphicsEnvironment.isHeadless()) {
109 component.getInputMap().put(
110 KeyStroke.getKeyStroke(KeyEvent.VK_Z, PlatformManager.getPlatform().getMenuShortcutKeyMaskEx()), undoAction);
111 component.getInputMap().put(
112 KeyStroke.getKeyStroke(KeyEvent.VK_Y, PlatformManager.getPlatform().getMenuShortcutKeyMaskEx()), redoAction);
113 }
114 undoRedo = true;
115 }
116 }
117
118 private void disableUndoRedo() {
119 if (undoRedo) {
120 if (!GraphicsEnvironment.isHeadless()) {
121 component.getInputMap().remove(
122 KeyStroke.getKeyStroke(KeyEvent.VK_Z, PlatformManager.getPlatform().getMenuShortcutKeyMaskEx()));
123 component.getInputMap().remove(
124 KeyStroke.getKeyStroke(KeyEvent.VK_Y, PlatformManager.getPlatform().getMenuShortcutKeyMaskEx()));
125 }
126 component.getDocument().removeUndoableEditListener(undoEditListener);
127 undoRedo = false;
128 }
129 }
130
131 private void addMenuEntries() {
132 if (component.isEditable()) {
133 if (undoRedo) {
134 add(new JMenuItem(undoAction));
135 add(new JMenuItem(redoAction));
136 addSeparator();
137 }
138 addMenuEntry(component, tr("Cut"), DefaultEditorKit.cutAction, null);
139 }
140 addMenuEntry(component, tr("Copy"), DefaultEditorKit.copyAction, "copy");
141 if (component.isEditable()) {
142 addMenuEntry(component, tr("Paste"), DefaultEditorKit.pasteAction, "paste");
143 addMenuEntry(component, tr("Delete"), DefaultEditorKit.deleteNextCharAction, null);
144 }
145 addSeparator();
146 addMenuEntry(component, tr("Select All"), DefaultEditorKit.selectAllAction, null);
147 }
148
149 /**
150 * Detaches this contextual menu from its text component.
151 * @return {@code this}
152 * @see #attach(JTextComponent, boolean)
153 */
154 protected TextContextualPopupMenu detach() {
155 if (isAttached()) {
156 component.removePropertyChangeListener(EDITABLE, propertyChangeListener);
157 removeAll();
158 if (undoRedo) {
159 disableUndoRedo();
160 }
161 component = null;
162 }
163 return this;
164 }
165
166 /**
167 * Creates a new {@link TextContextualPopupMenu} and enables it for the given text component.
168 * @param component The component that will display the menu and handle its actions.
169 * @param undoRedo Enables or not Undo/Redo feature. Not recommended for table cell editors, unless each cell provides its own editor
170 * @return The {@link PopupMenuLauncher} responsible of displaying the popup menu.
171 * Call {@link #disableMenuFor} with this object if you want to disable the menu later.
172 * @see #disableMenuFor
173 */
174 public static PopupMenuLauncher enableMenuFor(JTextComponent component, boolean undoRedo) {
175 PopupMenuLauncher launcher = new PopupMenuLauncher(new TextContextualPopupMenu().attach(component, undoRedo), true);
176 component.addMouseListener(launcher);
177 return launcher;
178 }
179
180 /**
181 * Disables the {@link TextContextualPopupMenu} attached to the given popup menu launcher and text component.
182 * @param component The component that currently displays the menu and handles its actions.
183 * @param launcher The {@link PopupMenuLauncher} obtained via {@link #enableMenuFor}.
184 * @see #enableMenuFor
185 */
186 public static void disableMenuFor(JTextComponent component, PopupMenuLauncher launcher) {
187 if (launcher.getMenu() instanceof TextContextualPopupMenu) {
188 ((TextContextualPopupMenu) launcher.getMenu()).detach();
189 component.removeMouseListener(launcher);
190 }
191 }
192
193 /**
194 * Empties the internal undo manager.
195 * @since 14977
196 */
197 public void discardAllUndoableEdits() {
198 undo.discardAllEdits();
199 updateUndoRedoState();
200 }
201
202 /**
203 * Determines if this popup is currently attached to a component.
204 * @return {@code true} if this popup is currently attached to a component, {@code false} otherwise.
205 */
206 public final boolean isAttached() {
207 return component != null;
208 }
209
210 protected void addMenuEntry(JTextComponent component, String label, String actionName, String iconName) {
211 Action action = component.getActionMap().get(actionName);
212 if (action != null) {
213 JMenuItem mi = new JMenuItem(action);
214 mi.setText(label);
215 if (iconName != null && Config.getPref().getBoolean("text.popupmenu.useicons", true)) {
216 ImageIcon icon = loadIcon(iconName);
217 if (icon != null) {
218 mi.setIcon(icon);
219 }
220 }
221 add(mi);
222 }
223 }
224
225 protected class UndoAction extends AbstractAction {
226
227 /**
228 * Constructs a new {@code UndoAction}.
229 */
230 public UndoAction() {
231 super(tr("Undo"));
232 setEnabled(false);
233 }
234
235 @Override
236 public void actionPerformed(ActionEvent e) {
237 try {
238 undo.undo();
239 } catch (CannotUndoException ex) {
240 Logging.trace(ex);
241 } finally {
242 updateUndoState();
243 redoAction.updateRedoState();
244 }
245 }
246
247 public void updateUndoState() {
248 if (undo.canUndo()) {
249 setEnabled(true);
250 putValue(Action.NAME, undo.getUndoPresentationName());
251 } else {
252 setEnabled(false);
253 putValue(Action.NAME, tr("Undo"));
254 }
255 }
256 }
257
258 protected class RedoAction extends AbstractAction {
259
260 /**
261 * Constructs a new {@code RedoAction}.
262 */
263 public RedoAction() {
264 super(tr("Redo"));
265 setEnabled(false);
266 }
267
268 @Override
269 public void actionPerformed(ActionEvent e) {
270 try {
271 undo.redo();
272 } catch (CannotRedoException ex) {
273 Logging.trace(ex);
274 } finally {
275 updateRedoState();
276 undoAction.updateUndoState();
277 }
278 }
279
280 public void updateRedoState() {
281 if (undo.canRedo()) {
282 setEnabled(true);
283 putValue(Action.NAME, undo.getRedoPresentationName());
284 } else {
285 setEnabled(false);
286 putValue(Action.NAME, tr("Redo"));
287 }
288 }
289 }
290}
Note: See TracBrowser for help on using the repository browser.