Fixing Disappearing Text Selections when a Menu is Opened

Prior to Sun’s introduction of a new focus management system in version 1.4 of the Java platform, opening a menu resulted in text selections (in Swing components) appearing to be lost*. The selection reappears when the menu is released, and menu operations can still retrieve the selected text, but the behavior can be quite disconcerting for users. For example, a user intending to copy text would select the text and then open the Edit menu, only to see the selected text be deselected.

Java 1.4 introduces a more sophisticated mechanism for managing focus, which fixes this (and many other) focus oddities. Java 1.3 is, however, still an active platform. This is particularly true on the Macintosh, where, for example, the 1.4 implemention released with Panther (OSX 10.3) is still having various wrinkles ironed out of it. Additionally, for many applications there are few enough differences between 1.3 and 1.4 that it is in the developer’s interest to support both versions concurrently.

Fortunately, there is a simple fix under 1.3. The culprit is the behavior of the default implementation of the javax.swing.text.Caret implementation used in all of the javax.swing.text.JTextComponent subclasses: JTextField, JTextArea, JTextPane, and JEditorPane.

javax.swing.text.DefaultCaret is registered as a FocusListener on the text component. When a menu is opened, DefaultCaret.focusLost(FocusEvent e) hides the caret itself, along with the selection. The fix is to just hide the caret, but defer hiding the selection until another text component gets focus. The code snippet below shows a DefaultCaret subclass that fixes this problem:



import javax.swing.UIManager;
import javax.swing.text.DefaultCaret;
import java.awt.event.FocusEvent;

/**
 * Caret implementation that doesn't blow away the selection when
 * we lose focus.
 */
public class SelectionPreservingCaret extends DefaultCaret {
    /**
     * The last SelectionPreservingCaret that lost focus
     */
    private static SelectionPreservingCaret last = null;

    /**
     * The last event that indicated loss of focus
     */
    private static FocusEvent lastFocusEvent = null;

    public SelectionPreservingCaret() {
        // The blink rate is set by BasicTextUI when the text component
        // is created, and is not (re-) set when a new Caret is installed.
        // This implementation attempts to pull a value from the UIManager,
        // and defaults to a 500ms blink rate. This assumes that the
        // look and feel uses the same blink rate for all text components
        // (and hence we just pull the value for TextArea). If you are
        // using a look and feel for which this is not the case, you may
        // need to set the blink rate after creating the Caret.
        int blinkRate = 500;
        Object o = UIManager.get("TextArea.caretBlinkRate");
        if ((o != null) && (o instanceof Integer)) {
            Integer rate = (Integer) o;
            blinkRate = rate.intValue();
        }
        setBlinkRate(blinkRate);
    }

    /**
     * Called when the component containing the caret gains focus. 
     * DefaultCaret does most of the work, while the subclass checks
     * to see if another instance of SelectionPreservingCaret previously
     * had focus.
     *
     * @param e the focus event
     * @see java.awt.event.FocusListener#focusGained
     */
    public void focusGained(FocusEvent evt) {
        super.focusGained(evt);

        // If another instance of SelectionPreservingCaret had focus and
        // we defered a focusLost event, deliver that event now.
        if ((last != null) && (last != this)) {
            last.hide();
        }
    }

    /**
     * Called when the component containing the caret loses focus. Instead
     * of hiding both the caret and the selection, the subclass only 
     * hides the caret and saves a (static) reference to the event and this
     * specific caret instance so that the event can be delivered later
     * if appropriate.
     *
     * @param e the focus event
     * @see java.awt.event.FocusListener#focusLost
     */
    public void focusLost(FocusEvent evt) {
        setVisible(false);
        last = this;
        lastFocusEvent = evt;
    }

    /**
     * Delivers a defered focusLost event to this caret.
     */
    protected void hide() {
        if (last == this) {
            super.focusLost(lastFocusEvent);
            last = null;
            lastFocusEvent = null;
        }
    }
}



Figure 1. SelectionPreservingCaret.java

By defering selection hiding, we may sure that selections stay in place until another text component receives focus. This makes the caret implementation slightly more complicated, but also ensures that two text components are not simultaneously showing a selection.

To use this class, you will need to explicitly (re-)set the Caret for each JTextComponent that you instantiate.



    ....
    JTextPane text = new JTextPane();
    text.setCaret(new SelectionPreservingCaret());
    ....



Figure 2. Using SelectionPreservingCaret

This fix has several potential drawbacks:


  1. You must explicitly set the caret on each text field, or at least on each text field which may be active when a menu is opened. If you are using a custom look and feel that subclasses from Swing’s basic look and feel (as most do), you could provide an alternate implementation of javax.swing.plaf.basic.BasicTextUI.createCaret() that returns an instance of SelectionPreservingCaret instead of the trivial subclass of DefaultCaret that BasicTextUI returns.
  2. Static references (such as those used to store the last caret instance that received a focusLost event, and the event itself) are always potential sources of memory leaks. This will not be a problem in this case, since the fields are likely to have their values reset fairly often.
  3. Future-proofing: SelectionPreservingCaret fixes a problem in Java 1.3 and does no (known) harm in Java 1.4, though it will waste a few cycles and a tiny amount of memory. Whether it will cause problems in, for example, Java 1.9 is an unkown. Version checking will be tedious, though implementing caret installation into a central location (a factory class or a static method, perhaps in SelectionPreservingCaret itself) may help future-proof the solution. If you are using a custom look and feel (as described in [1] above), the createCaret would also be a reasonable place for a version check.

* This issue was tracked in Sun’s bug database as bug ID 4244100. It was fixed in JDK 1.4, but the technique described above may still be useful if you want to preserve text selection in other cases where focus is lost.