Search code examples
javaswingjbuttonnimbus

How to override Nimbus button margins for a single button


I have created a text field component with an 'x' button inside that will clear the field's text. It works great when using Window's system look and feel (for Windows), however I am having problems trying to get the right look when using Nimbus - namely, I can't change the margins on the button using button.setMargin(new Inset(0, 0, 0, 0) as I can with the system look and feel.

I did look for help setting the margins when using Nimbus and it appears to be a common problem. I was able to find a partial solution that does allow me to change the margins, however it changes the margins for all buttons and I want only the single button to be different than the default.

I set the 'x' button size explicitly, calculated from the bounds of the 'x' string so that it is just big enough to display the 'x'.

The result when using Window's system look and feel, which is the general appearance that I'm looking for:

System look and feel

The result when using the default Nimbus look and feel; 'x' overflows the space Nimbus allows for it, resulting in an ellipsis.

Nimbus look and feel

The result when using Nimbus and overriding the Button.contentMargins: Now the 'x' button looks correct, but the 'test' button looks awful.

Nimbus look and feel, plus margin overrides

Source code:

import java.awt.Graphics;
import java.awt.Insets;
import java.awt.geom.Rectangle2D;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.LookAndFeel;
import javax.swing.UIDefaults;
import javax.swing.UIManager;
import javax.swing.UIManager.LookAndFeelInfo;
import javax.swing.UnsupportedLookAndFeelException;

public class TextFieldTest {        
    public static void main(String[] args) {
        boolean useNimbusLookAndFeel    = false;
        boolean overrideNimbusDefaults  = false;
        String lafName = UIManager.getSystemLookAndFeelClassName();

        if ( useNimbusLookAndFeel ) {
            for ( LookAndFeelInfo info : UIManager.getInstalledLookAndFeels() ) {
                if ( "Nimbus".equals(info.getName()) ) {
                    lafName = info.getClassName();      
                    break;
                }
            }
        }

        try {
            UIManager.setLookAndFeel(lafName);              
        }
        catch (ClassNotFoundException e) {}
        catch (InstantiationException e) {}
        catch (IllegalAccessException e) {}
        catch (UnsupportedLookAndFeelException e) {}

        //Code to set override default button insets for Nimbus:
        if ( useNimbusLookAndFeel && overrideNimbusDefaults ) {
            LookAndFeel laf = UIManager.getLookAndFeel();               
            if ( laf != null ) {
                UIDefaults def = laf.getDefaults();
                def.put("Button.contentMargins", new Insets(0,0,0,0));
            }
        }       

        //Create frame:
        JPanel panel = new JPanel();        
        panel.add(new JButton("test"));
        panel.add(new DeletableTextField());

        JFrame frame = new JFrame();
        frame.getContentPane().add(panel);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.pack();
        frame.setVisible(true);
    }   

    static class DeletableTextField extends JTextField {
        JButton         mButton     = null;

        public DeletableTextField() {
            super(30);
            setup();
        }

        private void setup() {
            mButton = new JButton("x");
            mButton.setMargin(new Insets(0, 0, 0, 0));
            add(mButton);
        }

        @Override
        public void paint(Graphics g) {
            g.setFont(mButton.getFont());
            Rectangle2D rect    = g.getFontMetrics().getStringBounds(mButton.getText(), g);
            int stringWidth     = (int)rect.getWidth();

            int topOffset       = 3;
            int rightOffset     = 3;
            int bottomOffset    = 3;
            int width           = (int)Math.ceil(stringWidth * 2.5);
            int height          = getHeight() - (bottomOffset + topOffset);         
            int x_coord         = getWidth() - (rightOffset + width);

            mButton.setMargin(new Insets(0, 0, 0, 0));
            mButton.setBounds(x_coord, topOffset, width, height);

            super.paint(g);
        }
    }
}

I am aware that I can accomplish this in other ways, like painting my own 'x' on top, but I thought it would be nice to take advantage of the current look-and-feel's button painting abilities for a unified look.

So I would like to ask if there is any way to change a single JButton's margins when using the Nimbus look and feel without changing the margins for all the buttons. Alternatively, if there's already a swing component out there that looks similar (text field with 'x' button), I may be able to use that instead.


Solution

  • Nimbus supports per-instance overrides of LAF properties, in your context something like:

      mButton = new JButton("x");
      UIDefaults def = new UIDefaults();
      def.put("Button.contentMargins", new Insets(0,0,0,0));
      mButton.putClientProperty("Nimbus.Overrides", def);