javaswingcontextmenujtextcomponent

Adding a context menu to all Swing text components in application


Swing text components don't have a context menu with cut/copy/paste/etc. I want to add one so it behaves more fluently and like a native app. I've written a menu for this and it works fine. I add it to each text box using:

someTextBox.setComponentPopupMenu(TextContextMenu.INSTANCE);

The thing is, adding this everywhere is annoying. Secondly, if I forget it for a text box somewhere, the application is inconsistent. Thirdly, I can't add it for text boxes where I don't control the creation code, like the ones from JOptionPane.showInputDialog or JFileChooser dialogs.

Is there any way to override the default context menu of JTextComponent application-wide? I know this would be a form of spooky action at a distance but I'm okay with that. Comments on the menu itself are also welcome.

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.text.JTextComponent;

public class TextContextMenu extends JPopupMenu implements ActionListener {
    public static final TextContextMenu INSTANCE = new TextContextMenu();
    private final JMenuItem itemCut;
    private final JMenuItem itemCopy;
    private final JMenuItem itemPaste;
    private final JMenuItem itemDelete;
    private final JMenuItem itemSelectAll;

    private TextContextMenu() {
        itemCut = newItem("Cut", 'T');
        itemCopy = newItem("Copy", 'C');
        itemPaste = newItem("Paste", 'P');
        itemDelete = newItem("Delete", 'D');
        addSeparator();
        itemSelectAll = newItem("Select All", 'A');
    }

    private JMenuItem newItem(String text, char mnemonic) {
        JMenuItem item = new JMenuItem(text, mnemonic);
        item.addActionListener(this);
        return add(item);
    }

    @Override
    public void show(Component invoker, int x, int y) {
        JTextComponent tc = (JTextComponent)invoker;
        boolean changeable = tc.isEditable() && tc.isEnabled();
        itemCut.setVisible(changeable);
        itemPaste.setVisible(changeable);
        itemDelete.setVisible(changeable);
        super.show(invoker, x, y);
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        JTextComponent tc = (JTextComponent)getInvoker();
        tc.requestFocus();

        boolean haveSelection = tc.getSelectionStart() != tc.getSelectionEnd();
        if (e.getSource() == itemCut) {
            if (!haveSelection) tc.selectAll();
            tc.cut();
        } else if (e.getSource() == itemCopy) {
            if (!haveSelection) tc.selectAll();
            tc.copy();
        } else if (e.getSource() == itemPaste) {
            tc.paste();
        } else if (e.getSource() == itemDelete) {
            if (!haveSelection) tc.selectAll();
            tc.replaceSelection("");
        } else if (e.getSource() == itemSelectAll) {
            tc.selectAll();
        }
    }
}

Solution

  • I've figured out how to do this application-wide, including on JFileChoosers and showInputDialog and things! I'm not sure how sane and proper it is but it works. It (ab)uses the pluggable look and feel system. JTextComponent calls updateUI during its constructor, which provides the opportunity to call setComponentPopupMenu when the L&F gets asked for its UI delegate.

    If you change the look and feel for already-open windows, each component's updateUI method will be called again. To prevent setting the default menu again, the code below stores the property of whether a text box has already been initialized or not using JComponent.putClientProperty.

    The net effect is it behaves just as though each JTextComponent itself was calling setComponentPopupMenu just one time during its constructor. Thus, it is easy to override this for special text boxes which want no menu or want a different menu: just call setComponentPopupMenu again. E.g, from a textfield subclass constructor or from the calling code that creates a window and its widgets.

    This is the code to run once at application startup:

    UIManager.addAuxiliaryLookAndFeel(new LookAndFeel() {
        private final UIDefaults defaults = new UIDefaults() {
            @Override
            public javax.swing.plaf.ComponentUI getUI(JComponent c) {
                if (c instanceof javax.swing.text.JTextComponent) {
                    if (c.getClientProperty(this) == null) {
                        c.setComponentPopupMenu(TextContextMenu.INSTANCE);
                        c.putClientProperty(this, Boolean.TRUE);
                    }
                }
                return null;
            }
        };
        @Override public UIDefaults getDefaults() { return defaults; };
        @Override public String getID() { return "TextContextMenu"; }
        @Override public String getName() { return getID(); }
        @Override public String getDescription() { return getID(); }
        @Override public boolean isNativeLookAndFeel() { return false; }
        @Override public boolean isSupportedLookAndFeel() { return true; }
    });