001 /*
002 * Copyright 2002-2004 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005 * use this file except in compliance with the License. You may obtain a copy of
006 * the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013 * License for the specific language governing permissions and limitations under
014 * the License.
015 */
016 package org.springframework.richclient.text;
017
018 import java.awt.Toolkit;
019 import java.awt.event.ActionEvent;
020 import java.awt.event.ActionListener;
021 import java.awt.event.FocusEvent;
022 import java.awt.event.FocusListener;
023 import java.awt.event.MouseAdapter;
024 import java.awt.event.MouseEvent;
025 import java.util.Enumeration;
026 import java.util.Hashtable;
027 import java.util.Vector;
028
029 import javax.swing.Action;
030 import javax.swing.JPasswordField;
031 import javax.swing.JPopupMenu;
032 import javax.swing.KeyStroke;
033 import javax.swing.Timer;
034 import javax.swing.event.CaretEvent;
035 import javax.swing.event.CaretListener;
036 import javax.swing.event.UndoableEditEvent;
037 import javax.swing.event.UndoableEditListener;
038 import javax.swing.text.JTextComponent;
039 import javax.swing.text.Keymap;
040 import javax.swing.undo.UndoManager;
041
042 import org.springframework.binding.value.CommitTrigger;
043 import org.springframework.binding.value.CommitTriggerListener;
044 import org.springframework.richclient.application.Application;
045 import org.springframework.richclient.application.ApplicationWindow;
046 import org.springframework.richclient.command.ActionCommand;
047 import org.springframework.richclient.command.CommandGroup;
048 import org.springframework.richclient.command.CommandManager;
049 import org.springframework.richclient.command.TargetableActionCommand;
050 import org.springframework.richclient.command.support.AbstractActionCommandExecutor;
051 import org.springframework.richclient.command.support.DefaultCommandManager;
052 import org.springframework.richclient.command.support.GlobalCommandIds;
053
054 /**
055 * Helper class that decorates a <code>JTextComponent</code> with a standard
056 * popup menu. Support for undo/redo is also provided.
057 *
058 * @author Oliver Hutchison
059 */
060 public class TextComponentPopup extends MouseAdapter implements FocusListener, CaretListener, UndoableEditListener {
061
062 /**
063 * Delay in ms between updates of the paste commands status. We only update
064 * the paste command's status occasionally as this is a quite expensive
065 * operation.
066 */
067 private static final int PAST_REFRESH_TIMER_DELAY = 100;
068
069 private static final String[] COMMANDS = new String[] { GlobalCommandIds.UNDO, GlobalCommandIds.REDO,
070 GlobalCommandIds.COPY, GlobalCommandIds.CUT, GlobalCommandIds.PASTE, GlobalCommandIds.SELECT_ALL };
071
072 public static void attachPopup(JTextComponent textComponent, CommitTrigger resetUndoHistoryTrigger) {
073 new TextComponentPopup(textComponent, resetUndoHistoryTrigger);
074 }
075
076 public static void attachPopup(JTextComponent textComponent) {
077 new TextComponentPopup(textComponent, null);
078 }
079
080 private final JTextComponent textComponent;
081
082 private final Timer updatePasteStatusTimer;
083
084 private final UndoManager undoManager = new UndoManager();
085
086 private final CommitTrigger resetUndoHistoryTrigger;
087
088 private static CommandManager localCommandManager;
089
090 private final UndoCommandExecutor undo = new UndoCommandExecutor();
091
092 private final RedoCommandExecutor redo = new RedoCommandExecutor();
093
094 private final CutCommandExecutor cut = new CutCommandExecutor();
095
096 private final CopyCommandExecutor copy = new CopyCommandExecutor();
097
098 private final PasteCommandExecutor paste = new PasteCommandExecutor();
099
100 private final SelectAllCommandExecutor selectAll = new SelectAllCommandExecutor();
101
102 protected TextComponentPopup(JTextComponent textComponent, CommitTrigger resetUndoHistoryTrigger) {
103 this.textComponent = textComponent;
104 this.resetUndoHistoryTrigger = resetUndoHistoryTrigger;
105 this.updatePasteStatusTimer = new Timer(PAST_REFRESH_TIMER_DELAY, new ActionListener() {
106 public void actionPerformed(ActionEvent e) {
107 updatePasteStatus();
108 }
109 });
110 updatePasteStatusTimer.setCoalesce(true);
111 updatePasteStatusTimer.setRepeats(false);
112 updatePasteStatusTimer.setInitialDelay(PAST_REFRESH_TIMER_DELAY);
113 registerListeners();
114 registerAccelerators();
115 }
116
117 private void registerListeners() {
118 textComponent.addMouseListener(this);
119 textComponent.addFocusListener(this);
120 textComponent.addCaretListener(this);
121 textComponent.getDocument().addUndoableEditListener(this);
122 if (resetUndoHistoryTrigger != null) {
123 CommitTriggerListener resetUndoHistoryHandler = new CommitTriggerListener() {
124 public void commit() {
125 undoManager.discardAllEdits();
126 updateUndoRedoState();
127 }
128
129 public void revert() {
130 }
131 };
132 resetUndoHistoryTrigger.addCommitTriggerListener(resetUndoHistoryHandler);
133 }
134 }
135
136 protected CommandManager getCommandManager() {
137 CommandManager commandManager;
138 ApplicationWindow appWindow = Application.instance().getActiveWindow();
139 if (appWindow == null || appWindow.getCommandManager() == null) {
140 if (localCommandManager == null) {
141 localCommandManager = new DefaultCommandManager();
142 }
143 commandManager = localCommandManager;
144 }
145 else {
146 commandManager = appWindow.getCommandManager();
147 }
148 for (int i = 0; i < COMMANDS.length; i++) {
149 if (!commandManager.containsActionCommand(COMMANDS[i])) {
150 commandManager.registerCommand(new TargetableActionCommand(COMMANDS[i], null));
151 }
152 }
153 return commandManager;
154 }
155
156 public void registerAccelerators() {
157 CommandManager commandManager = getCommandManager();
158 Keymap keymap = new DefaultKeymap(getClass().getName(), textComponent.getKeymap());
159 for (int i = 0; i < COMMANDS.length; i++) {
160 ActionCommand command = commandManager.getActionCommand(COMMANDS[i]);
161 keymap.addActionForKeyStroke(command.getAccelerator(), command.getActionAdapter());
162 }
163 if (COMMANDS.length > 0) {
164 textComponent.setKeymap(keymap);
165 }
166 }
167
168 /**
169 * @see java.awt.event.MouseListener#mousePressed(java.awt.event.MouseEvent)
170 */
171 public void mousePressed(MouseEvent evt) {
172 maybeShowPopup(evt);
173 }
174
175 /**
176 * @see java.awt.event.MouseAdapter#mouseReleased(java.awt.event.MouseEvent)
177 */
178 public void mouseReleased(MouseEvent evt) {
179 maybeShowPopup(evt);
180 }
181
182 private void maybeShowPopup(MouseEvent evt) {
183 if (evt.isPopupTrigger()) {
184 updatePasteStatusNow();
185 createPopup().show(evt.getComponent(), evt.getX(), evt.getY());
186 }
187 }
188
189 public void caretUpdate(CaretEvent e) {
190 updateState();
191 }
192
193 public void focusGained(FocusEvent e) {
194 updateState();
195 registerCommandExecutors();
196 }
197
198 public void focusLost(FocusEvent e) {
199 if (!e.isTemporary()) {
200 unregisterCommandExecutors();
201 }
202 }
203
204 public void undoableEditHappened(UndoableEditEvent e) {
205 undoManager.addEdit(e.getEdit());
206 updateUndoRedoState();
207 }
208
209 private JPopupMenu createPopup() {
210 if (textComponent instanceof JPasswordField)
211 return getPasswordCommandGroup().createPopupMenu();
212
213 if (isEditable())
214 return getEditableCommandGroup().createPopupMenu();
215
216 return getReadOnlyCommandGroup().createPopupMenu();
217 }
218
219 private void updateState() {
220 boolean hasSelection = textComponent.getSelectionStart() != textComponent.getSelectionEnd();
221 copy.setEnabled(hasSelection);
222 selectAll.setEnabled(textComponent.getDocument().getLength() > 0);
223 boolean isEditable = isEditable();
224 cut.setEnabled(hasSelection && isEditable);
225 if (isEditable) {
226 scheduleUpdatePasteStatus();
227 }
228 else {
229 paste.setEnabled(false);
230 }
231 updateUndoRedoState();
232 }
233
234 private void updateUndoRedoState() {
235 undo.setEnabled(undoManager.canUndo());
236 redo.setEnabled(undoManager.canRedo());
237 }
238
239 private void scheduleUpdatePasteStatus() {
240 // we do this using a timer as the method canPasteFromClipboard()
241 // can be a schedule significant bottle neck when there's lots of typing
242 // going on
243 if (!updatePasteStatusTimer.isRunning()) {
244 updatePasteStatusTimer.restart();
245 }
246 }
247
248 private void updatePasteStatusNow() {
249 if (updatePasteStatusTimer.isRunning()) {
250 updatePasteStatusTimer.stop();
251 }
252 updatePasteStatus();
253 }
254
255 private void updatePasteStatus() {
256 paste.setEnabled(isEditable() && canPasteFromClipboard());
257 }
258
259 /**
260 * Try not to call this method to much as SystemClipboard#getContents()
261 * relatively slow.
262 */
263 private boolean canPasteFromClipboard() {
264 try {
265 return textComponent.getTransferHandler().canImport(
266 textComponent,
267 Toolkit.getDefaultToolkit().getSystemClipboard().getContents(textComponent)
268 .getTransferDataFlavors());
269 }
270 catch (IllegalStateException e) {
271 /*
272 * as the javadoc of Clipboard.getContents state: the
273 * IllegalStateException can be thrown when the clipboard is not
274 * available (i.e. in use by another application), so we return
275 * false.
276 */
277 return false;
278 }
279 }
280
281 private boolean isEditable() {
282 return !(textComponent instanceof JPasswordField) && textComponent.isEnabled() && textComponent.isEditable();
283 }
284
285 protected CommandGroup getEditableCommandGroup() {
286 CommandGroup editGroup = getCommandManager().getCommandGroup("textEditMenu");
287 if (editGroup == null) {
288 editGroup = getCommandManager().createCommandGroup(
289 "textEditMenu",
290 new Object[] { GlobalCommandIds.UNDO, GlobalCommandIds.REDO, "separator", GlobalCommandIds.CUT,
291 GlobalCommandIds.COPY, GlobalCommandIds.PASTE, "separator", GlobalCommandIds.SELECT_ALL });
292 }
293 return editGroup;
294 }
295
296 protected CommandGroup getPasswordCommandGroup() {
297 CommandGroup passwordGroup = getCommandManager().getCommandGroup("passwordTextEditMenu");
298 if (passwordGroup == null) {
299 passwordGroup = getCommandManager().createCommandGroup("passwordTextEditMenu",
300 new Object[] { GlobalCommandIds.UNDO, GlobalCommandIds.REDO });
301 }
302 return passwordGroup;
303 }
304
305 protected CommandGroup getReadOnlyCommandGroup() {
306 CommandGroup readOnlyGroup = getCommandManager().getCommandGroup("readOnlyTextEditMenu");
307 if (readOnlyGroup == null) {
308 readOnlyGroup = getCommandManager().createCommandGroup("readOnlyTextEditMenu",
309 new Object[] { GlobalCommandIds.COPY, "separator", GlobalCommandIds.SELECT_ALL });
310 }
311 return readOnlyGroup;
312 }
313
314 private void registerCommandExecutors() {
315 CommandManager commandManager = getCommandManager();
316 commandManager.setTargetableActionCommandExecutor(GlobalCommandIds.UNDO, undo);
317 commandManager.setTargetableActionCommandExecutor(GlobalCommandIds.REDO, redo);
318 commandManager.setTargetableActionCommandExecutor(GlobalCommandIds.CUT, cut);
319 commandManager.setTargetableActionCommandExecutor(GlobalCommandIds.COPY, copy);
320 commandManager.setTargetableActionCommandExecutor(GlobalCommandIds.PASTE, paste);
321 commandManager.setTargetableActionCommandExecutor(GlobalCommandIds.SELECT_ALL, selectAll);
322 }
323
324 private void unregisterCommandExecutors() {
325 CommandManager commandManager = getCommandManager();
326 commandManager.setTargetableActionCommandExecutor(GlobalCommandIds.UNDO, null);
327 commandManager.setTargetableActionCommandExecutor(GlobalCommandIds.REDO, null);
328 commandManager.setTargetableActionCommandExecutor(GlobalCommandIds.CUT, null);
329 commandManager.setTargetableActionCommandExecutor(GlobalCommandIds.COPY, null);
330 commandManager.setTargetableActionCommandExecutor(GlobalCommandIds.PASTE, null);
331 commandManager.setTargetableActionCommandExecutor(GlobalCommandIds.SELECT_ALL, null);
332 }
333
334 private class UndoCommandExecutor extends AbstractActionCommandExecutor {
335 public void execute() {
336 undoManager.undo();
337 }
338 }
339
340 private class RedoCommandExecutor extends AbstractActionCommandExecutor {
341 public void execute() {
342 undoManager.redo();
343 }
344 }
345
346 private class CutCommandExecutor extends AbstractActionCommandExecutor {
347 public void execute() {
348 textComponent.cut();
349 }
350 }
351
352 private class CopyCommandExecutor extends AbstractActionCommandExecutor {
353 public void execute() {
354 textComponent.copy();
355 }
356 }
357
358 private class PasteCommandExecutor extends AbstractActionCommandExecutor {
359 public void execute() {
360 textComponent.paste();
361 }
362 }
363
364 private class SelectAllCommandExecutor extends AbstractActionCommandExecutor {
365 public void execute() {
366 textComponent.selectAll();
367 }
368 }
369
370 /**
371 * We need this class since keymaps are shared in jvm This class is a 100%
372 * copy of the jdk class {@link JTextComponent#DefaultKeymap
373 */
374 public static class DefaultKeymap implements Keymap {
375
376 String nm;
377
378 Keymap parent;
379
380 Hashtable bindings;
381
382 Action defaultAction;
383
384 DefaultKeymap(String nm, Keymap parent) {
385 this.nm = nm;
386 this.parent = parent;
387 bindings = new Hashtable();
388 }
389
390 /**
391 * Fetch the default action to fire if a key is typed (ie a KEY_TYPED
392 * KeyEvent is received) and there is no binding for it. Typically this
393 * would be some action that inserts text so that the keymap doesn't
394 * require an action for each possible key.
395 */
396 public Action getDefaultAction() {
397 if (defaultAction != null) {
398 return defaultAction;
399 }
400 return (parent != null) ? parent.getDefaultAction() : null;
401 }
402
403 /**
404 * Set the default action to fire if a key is typed.
405 */
406 public void setDefaultAction(Action a) {
407 defaultAction = a;
408 }
409
410 public String getName() {
411 return nm;
412 }
413
414 public Action getAction(KeyStroke key) {
415 Action a = (Action) bindings.get(key);
416 if ((a == null) && (parent != null)) {
417 a = parent.getAction(key);
418 }
419 return a;
420 }
421
422 public KeyStroke[] getBoundKeyStrokes() {
423 KeyStroke[] keys = new KeyStroke[bindings.size()];
424 int i = 0;
425 for (Enumeration e = bindings.keys(); e.hasMoreElements();) {
426 keys[i++] = (KeyStroke) e.nextElement();
427 }
428 return keys;
429 }
430
431 public Action[] getBoundActions() {
432 Action[] actions = new Action[bindings.size()];
433 int i = 0;
434 for (Enumeration e = bindings.elements(); e.hasMoreElements();) {
435 actions[i++] = (Action) e.nextElement();
436 }
437 return actions;
438 }
439
440 public KeyStroke[] getKeyStrokesForAction(Action a) {
441 if (a == null) {
442 return null;
443 }
444 KeyStroke[] retValue = null;
445 // Determine local bindings first.
446 Vector keyStrokes = null;
447 for (Enumeration enum_ = bindings.keys(); enum_.hasMoreElements();) {
448 Object key = enum_.nextElement();
449 if (bindings.get(key) == a) {
450 if (keyStrokes == null) {
451 keyStrokes = new Vector();
452 }
453 keyStrokes.addElement(key);
454 }
455 }
456 // See if the parent has any.
457 if (parent != null) {
458 KeyStroke[] pStrokes = parent.getKeyStrokesForAction(a);
459 if (pStrokes != null) {
460 // Remove any bindings defined in the parent that
461 // are locally defined.
462 int rCount = 0;
463 for (int counter = pStrokes.length - 1; counter >= 0; counter--) {
464 if (isLocallyDefined(pStrokes[counter])) {
465 pStrokes[counter] = null;
466 rCount++;
467 }
468 }
469 if (rCount > 0 && rCount < pStrokes.length) {
470 if (keyStrokes == null) {
471 keyStrokes = new Vector();
472 }
473 for (int counter = pStrokes.length - 1; counter >= 0; counter--) {
474 if (pStrokes[counter] != null) {
475 keyStrokes.addElement(pStrokes[counter]);
476 }
477 }
478 }
479 else if (rCount == 0) {
480 if (keyStrokes == null) {
481 retValue = pStrokes;
482 }
483 else {
484 retValue = new KeyStroke[keyStrokes.size() + pStrokes.length];
485 keyStrokes.copyInto(retValue);
486 System.arraycopy(pStrokes, 0, retValue, keyStrokes.size(), pStrokes.length);
487 keyStrokes = null;
488 }
489 }
490 }
491 }
492 if (keyStrokes != null) {
493 retValue = new KeyStroke[keyStrokes.size()];
494 keyStrokes.copyInto(retValue);
495 }
496 return retValue;
497 }
498
499 public boolean isLocallyDefined(KeyStroke key) {
500 return bindings.containsKey(key);
501 }
502
503 public void addActionForKeyStroke(KeyStroke key, Action a) {
504 bindings.put(key, a);
505 }
506
507 public void removeKeyStrokeBinding(KeyStroke key) {
508 bindings.remove(key);
509 }
510
511 public void removeBindings() {
512 bindings.clear();
513 }
514
515 public Keymap getResolveParent() {
516 return parent;
517 }
518
519 public void setResolveParent(Keymap parent) {
520 this.parent = parent;
521 }
522
523 /**
524 * String representation of the keymap... potentially a very long
525 * string.
526 */
527 public String toString() {
528 return "Keymap[" + nm + "]" + bindings;
529 }
530
531 }
532 }