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    }