/* * Copyright (C) 2006-2009 Sun Microsystems, Inc. All rights reserved. * Copyright (C) 2011 Peransin Nicolas. All rights reserved. * Use is subject to license terms. */ package org.mypsycho.swing.app.beans; import java.awt.KeyboardFocusManager; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.FlavorEvent; import java.awt.datatransfer.FlavorListener; import java.awt.event.ActionEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.HashMap; import java.util.Map; import javax.swing.ActionMap; import javax.swing.JComponent; import javax.swing.event.CaretEvent; import javax.swing.event.CaretListener; import javax.swing.text.Caret; import javax.swing.text.DefaultEditorKit; import javax.swing.text.JTextComponent; import org.mypsycho.swing.app.Action; import org.mypsycho.swing.app.ApplicationContext; import org.mypsycho.swing.app.SwingBean; /** * An ActionMap class that defines cut/copy/paste/delete. * <p/> * This class only exists to paper over limitations in the standard JTextComponent * cut/copy/paste/delete javax.swing.Actions. The standard cut/copy Actions don't * keep their enabled property in sync with having the focus and (for copy) having * a non-empty text selection. The standard paste Action's enabled property doesn't * stay in sync with the current contents of the clipboard. The paste/copy/delete * actions must also track the JTextComponent editable property. * <p/> * The new cut/copy/paste/delete are installed lazily, when a JTextComponent gets * the focus, and before any other focus-change related work is done. See * updateFocusOwner(). * * @author Hans Muller (Hans.Muller@Sun.COM) * @author Scott Violet (Scott.Violet@Sun.COM) * @author Peransin Nicolas */ public class TextActions extends SwingBean { // Well-known JTextComponent public static final String cutAction = "cut"; public static final String copyAction = "copy"; public static final String pasteAction = "paste"; public static final String deleteAction = "delete"; private static final String MARKER_ACTION_KEY = "TextActions.markerAction"; // Property as defined in KeyboardFocusManager private static final String KFM_FOCUS_OWNER_PROP = "permanentFocusOwner"; // Property as defined in JTextComponent private static final String TXT_EDITABLE_PROP = "editable"; private static final String[] ACTION_NAMES = { cutAction, copyAction, pasteAction, deleteAction, DefaultEditorKit.selectAllAction }; private final ApplicationContext context; private final Map<String, Boolean> enableds = new HashMap<String, Boolean>(); private JComponent focusOwner = null; private final javax.swing.Action markerAction = new javax.swing.AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { } }; private final CaretListener textCaretLnr = new CaretListener() { @Override public void caretUpdate(CaretEvent e) { updateTextActions((JTextComponent) (e.getSource())); } }; private final PropertyChangeListener textLnr = new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent e) { String propertyName = e.getPropertyName(); if ((propertyName == null) || TXT_EDITABLE_PROP.equals(propertyName)) { updateTextActions((JTextComponent) (e.getSource())); } } }; private PropertyChangeListener keybFocusLnr = new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent e) { if (KFM_FOCUS_OWNER_PROP.equals(e.getPropertyName())) { JComponent oldOwner = getFocusOwner(); Object newValue = e.getNewValue(); JComponent newOwner = (newValue instanceof JComponent) ? (JComponent) newValue : null; updateFocusOwner(oldOwner, newOwner); setFocusOwner(newOwner); // updateAllProxyActions(oldOwner, newOwner); } } // /* For each proxyAction in each ApplicationActionMap, if // * the newFocusOwner's ActionMap includes an Action with the same // * name then bind the proxyAction to it, otherwise set the proxyAction's // * proxyBinding to null. [TBD: synchronize access to actionMaps] // */ // private void updateAllProxyActions(JComponent oldFocusOwner, JComponent newFocusOwner) { // if (newFocusOwner != null) { // ActionMap ownerActionMap = newFocusOwner.getActionMap(); // if (ownerActionMap != null) { // updateProxyActions(getActionMap(), ownerActionMap, newFocusOwner); // for (WeakReference<ApplicationActionMap> appAMRef : actionMaps.values()) { // ApplicationActionMap appAM = appAMRef.get(); // if (appAM == null) { // continue; // } // updateProxyActions(appAM, ownerActionMap, newFocusOwner); // } // } // } // } // // /* For each proxyAction in appAM: if there's an action with the same // * name in the focusOwner's ActionMap, then set the proxyAction's proxy // * to the matching Action. In other words: calls to the proxyAction // * (actionPerformed) will delegate to the matching Action. // */ // private void updateProxyActions(ApplicationActionMap appAM, ActionMap ownerActionMap, // JComponent focusOwner) { // for (ApplicationAction proxyAction : appAM.getProxyActions()) { // String proxyActionName = proxyAction.getName(); // javax.swing.Action proxy = ownerActionMap.get(proxyActionName); // if (proxy != null) { // proxyAction.setProxy(proxy); // proxyAction.setProxySource(focusOwner); // } else { // proxyAction.setProxy(null); // proxyAction.setProxySource(null); // } // } // } }; private FlavorListener clipboardLnr = new FlavorListener() { @Override public void flavorsChanged(FlavorEvent e) { JComponent c = getFocusOwner(); if (c instanceof JTextComponent) { updateTextActions((JTextComponent) c); } } }; public TextActions(ApplicationContext context) { this.context = context; KeyboardFocusManager kfm = KeyboardFocusManager.getCurrentKeyboardFocusManager(); kfm.addPropertyChangeListener(keybFocusLnr); getClipboard().addFlavorListener(clipboardLnr); } public final ApplicationContext getContext() { return context; } private Clipboard getClipboard() { return getContext().getClipboard(); } /** * Returns the application's focus owner. * * @return The application's focus owner. */ public JComponent getFocusOwner() { return focusOwner; } /** * Changes the application's focus owner. * * @param focusOwner new focus owner */ void setFocusOwner(JComponent focusOwner) { Object oldValue = this.focusOwner; this.focusOwner = focusOwner; firePropertyChange("focusOwner", oldValue, this.focusOwner); } /* Called by the KeyboardFocus PropertyChangeListener in ApplicationContext, * before any other focus-change related work is done. */ void updateFocusOwner(JComponent oldOwner, JComponent newOwner) { if (oldOwner instanceof JTextComponent) { JTextComponent text = (JTextComponent) oldOwner; text.removeCaretListener(textCaretLnr); text.removePropertyChangeListener(textLnr); } if (newOwner instanceof JTextComponent) { JTextComponent text = (JTextComponent) newOwner; maybeInstallTextActions(text); updateTextActions(text); text.addCaretListener(textCaretLnr); text.addPropertyChangeListener(textLnr); } else if (newOwner == null) { for (String actionName : ACTION_NAMES) { setEnabled(actionName, false); } } } private void updateTextActions(JTextComponent text) { Caret caret = text.getCaret(); final int dot = caret.getDot(); final int mark = caret.getMark(); boolean selection = (dot != mark); boolean editable = text.isEditable(); setEnabled(copyAction, selection); setEnabled(cutAction, editable && selection); setEnabled(deleteAction, editable && selection); final int length = text.getDocument().getLength(); boolean allSelected = Math.abs(mark - dot) == length; setEnabled(DefaultEditorKit.selectAllAction, editable && allSelected); try { boolean stringCb = getClipboard().isDataFlavorAvailable(DataFlavor.stringFlavor); setEnabled(pasteAction, editable && stringCb); } catch (IllegalStateException e) { //ignore setEnabled(pasteAction, editable); } } // TBD: what if text.getActionMap is null, // or if it's parent isn't the UI-installed actionMap private void maybeInstallTextActions(JTextComponent text) { ActionMap actionMap = text.getActionMap(); if (actionMap.get(MARKER_ACTION_KEY) != markerAction) { actionMap.put(MARKER_ACTION_KEY, markerAction); for (Object key : ACTION_NAMES) { actionMap.put(key, getContext().getActionMap().get(key)); } } } private void invokeTextAction(String actionName, ActionEvent e) { if (!(focusOwner instanceof JTextComponent)) { return; } JTextComponent text = (JTextComponent) focusOwner; // We expect the parent.actionMap to be the one installed by the component-UI ActionMap actionMap = text.getActionMap().getParent(); ActionEvent actionEvent = new ActionEvent(text, ActionEvent.ACTION_PERFORMED, actionName, e.getWhen(), e.getModifiers()); actionMap.get(actionName).actionPerformed(actionEvent); } @Action(enabled = "enabled(" + cutAction + ")") public void cut(ActionEvent e) { invokeTextAction(cutAction, e); } @Action(enabled = "enabled(" + copyAction + ")") public void copy(ActionEvent e) { invokeTextAction(copyAction, e); } @Action(enabled = "enabled(" + pasteAction + ")") public void paste(ActionEvent e) { invokeTextAction(pasteAction, e); } @Action(enabled = "enabled(" + deleteAction + ")") public void delete(ActionEvent e) { /* The DefaultEditorKit.deleteNextCharAction is bound to the delete * key in text components. The name appears to be a misnomer, * however it's really a compromise. Calling the method * by a more accurate name, * "IfASelectionExistsThenDeleteItOtherwiseDeleteTheNextCharacter" * would be rather unwieldy. */ invokeTextAction(DefaultEditorKit.deleteNextCharAction, e); } @Action(enabled = "enabled(" + DefaultEditorKit.selectAllAction + ")") public void selectAll(ActionEvent e) { invokeTextAction(DefaultEditorKit.selectAllAction, e); } /** * Returns the enableds. * * @return the enableds */ public boolean getEnabled(String prop) { Boolean b = enableds.get(prop); return b != null ? b : false; } /** * Sets the enableds. * * @param enableds the enableds to set */ public void setEnabled(String prop, boolean enabled) { boolean old = getEnabled(prop); enableds.put(prop, enabled); firePropertyChange("enabled(" + prop + ")", old, enabled); } }