Question

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();
        }
    }
}
Was it helpful?

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; }
});

OTHER TIPS

You have two choices:

  1. If your application have an own customized Look and Feel you can simply do it in installUI(JComponent c) method in your TextUI(s).

  2. If you have no own L&F, you must traverse the Component tree, find all instances of JTextComponent, and add your context menu. I would make it as a static helper method. For dialogs and frames you need a basic class, which redefines the method setVisible() to add the context menu for all text components. All your custom dialogs or frame must extend the basic dialog/frame.

Because of the conflict I ran into with JavaHelp, I needed a different solution. What I came up with is a variation of choice 2. in the second answer, where I do the work when a class is instantiated rather than on a call to setVisible().

public static void installDefaultTextContextMenus( Container aContainer )
    {
        if ( aContainer != null )
        {
            if ( aContainer instanceof JFrame )
            {
                aContainer = ((JFrame)aContainer).getContentPane();
            }
            else if ( aContainer instanceof JDialog )
            {
                aContainer = ((JDialog)aContainer).getContentPane();
            }
            Component[] lComponents = aContainer.getComponents();
            for ( int lCompNum = 0; lCompNum < lComponents.length; ++lCompNum )
            {
                lComponents[ lCompNum ].getClass();
                if ( ( lComponents[ lCompNum ] instanceof JPanel ) ||
                     ( lComponents[ lCompNum ] instanceof JInternalFrame ) ||
                     ( lComponents[ lCompNum ] instanceof JScrollPane ) ||
                     ( lComponents[ lCompNum ] instanceof JSplitPane ) ||
                     ( lComponents[ lCompNum ] instanceof JTabbedPane ) ||
                     ( lComponents[ lCompNum ] instanceof Panel ) ||
                     ( lComponents[ lCompNum ] instanceof ScrollPane ) ||
                     ( lComponents[ lCompNum ] instanceof JViewport ) ||
                     ( lComponents[ lCompNum ] instanceof JFrame ) ||
                     ( lComponents[ lCompNum ] instanceof JDialog ) )
                {
                    installDefaultTextContextMenus( (Container)lComponents[ lCompNum ] );
                }
                else if ( lComponents[ lCompNum ] instanceof JTextComponent )
                {
                    ((JTextComponent)lComponents[ lCompNum ]).setComponentPopupMenu( TextContextMenu.INSTANCE );
                }
                else if ( lComponents[ lCompNum ] instanceof JComboBox )
                {
                    Component lEditorComp = ((JComboBox)lComponents[ lCompNum ]).getEditor().getEditorComponent();
                    if ( lEditorComp instanceof JTextComponent )
                    {
                        ((JTextComponent)lEditorComp).setComponentPopupMenu( TextContextMenu.INSTANCE );
                    }
                }
            }
        }
    }

The method will traverse practically any tree of Swing components. Luckily I already had a common method used to decorate all of my application's top-level JFrames, to which I was able to add a call to this method. I did have to add a call to this method for each instantiation of a frame or dialog not handled by my top-level menu, however.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top