Custom button not working on mac (ButtonUI)

I've got an application that uses custom buttons all over the place for text and icons. Works great for windows and linux, but now OSX users are complaining. Text doesn't display on the mac, just '...'. The code seems simple enough, but I'm clueless when it comes to macs. How can I fix this?

Test case:

package example.swingx;
import example.utils.ResourceLoader;
import com.xduke.xlayouts.XTableLayout;    
import javax.swing.*;
import javax.swing.plaf.ComponentUI;
import java.awt.*;
import java.awt.event.ActionEvent;

public class SmallButton extends JButton {
    private static ComponentUI ui = new SmallButtonUI();
    public SmallButton() {
        super();
        /*  final RepaintManager repaintManager = RepaintManager.currentManager(this);
        repaintManager.setDoubleBufferingEnabled(false);
        setDebugGraphicsOptions(DebugGraphics.FLASH_OPTION);*/
    }
    public SmallButton(AbstractAction action) {
        super(action);
    }
    public SmallButton(String text) {
        super(text);
    }
    public SmallButton(Icon icon) {
        super(icon);
    }
    public void updateUI() {
        setUI(ui);
    }

    public static void main(String[] args) {
        JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        final JPanel buttonPanel = new JPanel(new XTableLayout());
        SmallButton firstSmallButton = new SmallButton("One");
        SmallButton secondSmallButton = new SmallButton("Two");
        SmallButton thirdSmallButton = new SmallButton();

        ImageIcon cameraIcon = (ImageIcon) ResourceLoader.getIcon("camera");
        SmallButton fourth = new SmallButton();

        fourth.setAction(new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                System.out.println("Fourth button pressed!");
            }
        });
        fourth.setIcon(cameraIcon);

        buttonPanel.add(firstSmallButton, "+");
        buttonPanel.add(secondSmallButton, "+");
        buttonPanel.add(thirdSmallButton, "+");
        buttonPanel.add(fourth, "+");

        final Container container = frame.getContentPane();
        container.add(buttonPanel);
        frame.pack();
        frame.setVisible(true);
    }
}

UI:

package example.swingx;

import javax.swing.*;
import javax.swing.plaf.ComponentUI;
import javax.swing.plaf.basic.BasicButtonUI;
import java.awt.*;

public class SmallButtonUI extends BasicButtonUI {
    private static final Color FOCUS_COLOR = new Color(0, 0, 0);
    private static final Color BACKGROUND_COLOR = new Color(173, 193, 226);
    private static final Color SELECT_COLOR = new Color(102, 132, 186);
    private static final Color DISABLE_TEXT_COLOR = new Color(44, 44, 61);
    private static final Insets DEFAULT_SMALLBUTTON_MARGIN = new Insets(2, 4, 2, 4);

    private final static SmallButtonUI smallButtonUI = new SmallButtonUI();

    public static ComponentUI createUI(JComponent component) {
        return smallButtonUI;
    }
    protected Color getSelectColor() {
        return SELECT_COLOR;
    }
    protected Color getDisabledTextColor() {
        return DISABLE_TEXT_COLOR;
    }
    protected Color getFocusColor() {
        return FOCUS_COLOR;
    }

    public void paint(Graphics g, JComponent c) {
        ((Graphics2D) g).setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        super.paint(g, c);
    }

    protected void paintButtonPressed(Graphics graphics, AbstractButton button) {
        if (button.isContentAreaFilled()) {
            Dimension size = button.getSize();
            graphics.setColor(getSelectColor());
            graphics.fillRect(0, 0, size.width, size.height);
        }
    }

    public Dimension getMinimumSize(JComponent component) {
        final AbstractButton button = ((AbstractButton) component);

        // Handle icon buttons:
        Icon buttonIcon = button.getIcon();
        if (buttonIcon != null) {
            return new Dimension(
                    buttonIcon.getIconWidth(),
                    buttonIcon.getIconHeight()
            );
        }

        // Handle text buttons:
        final Font fontButton = button.getFont();
        final FontMetrics fontMetrics = button.getFontMetrics(fontButton);
        final String buttonText = button.getText();
        if (buttonText != null) {
            final int buttonTextWidth = fontMetrics.stringWidth(buttonText);
            return new Dimension(buttonTextWidth + 15,
                    fontMetrics.getHeight() + 5);
        }
        return null;
    }

    protected void installDefaults(AbstractButton button) {
        super.installDefaults(button);
        button.setMargin(DEFAULT_SMALLBUTTON_MARGIN);
        button.setBackground(getBackgroundColor());
    }
    private Color getBackgroundColor() {
        return BACKGROUND_COLOR;
    }
    public Dimension getPreferredSize(JComponent component) {
        return getMinimumSize(component);
    }
    public Dimension getMaximumSize(JComponent component) {
        return super.getMinimumSize(component);
    }
    public SmallButtonUI() {
        super();
    }
}

EDIT: After debugging, it seems getMinimumSize() is the same on both platforms. Also, when I stop on anywhere Graphics is used, it seems that the mac has a transY value of 47, while linux has 0. This 47 seems to feed into the clipping regions as well. Where could that be getting set?\


Solution 1:

Your calculation of preferred size is incorrect.

BasicButtonUI uses SwingUtilities.layoutCompoundLabel, examined here. In a label, the ellipsis is added if the string is too long, but a button is typically sized to fit its entire text.

Absent a better understanding of your context, I would use a sizeVariant, shown below. I've also shown a simple BasicButtonUI example using a smaller, derived Font. The UI menu can be used in conjunction with Quaqua for testing.

image

import java.awt.Color;
import java.awt.Component;
import java.awt.EventQueue;
import java.awt.FlowLayout;
import java.awt.Graphics;
import java.awt.GridLayout;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.List;
import javax.swing.AbstractButton;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JToolBar;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.plaf.basic.BasicButtonUI;

/**
 * @see https://stackoverflow.com/a/14599176/230513
 * @see https://stackoverflow.com/a/11949899/230513
 */
public class Test {

    private void display() {
        JFrame f = new JFrame("Test");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.setBackground(new Color(0xfff0f0f0));
        f.setLayout(new GridLayout(0, 1));
        f.add(createToolBar(f));
        f.add(variantPanel("mini"));
        f.add(variantPanel("small"));
        f.add(variantPanel("regular"));
        f.add(variantPanel("large"));
        JPanel customPanel = new JPanel();
        customPanel.add(createCustom("One"));
        customPanel.add(createCustom("Two"));
        customPanel.add(createCustom("Three"));
        f.add(customPanel);
        f.pack();
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    }

    private static JPanel variantPanel(String size) {
        JPanel variantPanel = new JPanel();
        variantPanel.add(createVariant("One", size));
        variantPanel.add(createVariant("Two", size));
        variantPanel.add(createVariant("Three", size));
        return variantPanel;
    }

    private static JButton createVariant(String name, String size) {
        JButton b = new JButton(name);
        b.putClientProperty("JComponent.sizeVariant", size);
        return b;
    }

    private static JButton createCustom(String name) {
        JButton b = new JButton(name) {

            @Override
            public void updateUI() {
                super.updateUI();
                setUI(new CustomButtonUI());
            }
        };
        return b;
    }

    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                new Test().display();
            }
        });
    }

    private static class CustomButtonUI extends BasicButtonUI {

        private static final Color BACKGROUND_COLOR = new Color(173, 193, 226);
        private static final Color SELECT_COLOR = new Color(102, 132, 186);

        @Override
        protected void paintText(Graphics g, AbstractButton b, Rectangle r, String t) {
            super.paintText(g, b, r, t);
            g.setColor(SELECT_COLOR);
            g.drawRect(r.x, r.y, r.width, r.height);
        }

        @Override
        protected void paintFocus(Graphics g, AbstractButton b,
            Rectangle viewRect, Rectangle textRect, Rectangle iconRect) {
            super.paintFocus(g, b, viewRect, textRect, iconRect);
            g.setColor(Color.blue.darker());
            g.drawRect(viewRect.x, viewRect.y, viewRect.width, viewRect.height);
        }

        @Override
        protected void paintButtonPressed(Graphics g, AbstractButton b) {
            if (b.isContentAreaFilled()) {
                g.setColor(SELECT_COLOR);
                g.fillRect(0, 0, b.getWidth(), b.getHeight());
            }
        }

        @Override
        protected void installDefaults(AbstractButton b) {
            super.installDefaults(b);
            b.setFont(b.getFont().deriveFont(11f));
            b.setBackground(BACKGROUND_COLOR);
        }

        public CustomButtonUI() {
            super();
        }
    }

    private static JToolBar createToolBar(final Component parent) {
        final UIManager.LookAndFeelInfo[] available =
            UIManager.getInstalledLookAndFeels();
        List<String> names = new ArrayList<String>();
        for (UIManager.LookAndFeelInfo info : available) {
            names.add(info.getName());
        }
        final JComboBox combo = new JComboBox(names.toArray());
        String current = UIManager.getLookAndFeel().getName();
        combo.setSelectedItem(current);
        combo.addActionListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent ae) {
                int index = combo.getSelectedIndex();
                try {
                    UIManager.setLookAndFeel(
                        available[index].getClassName());
                    SwingUtilities.updateComponentTreeUI(parent);
                } catch (Exception e) {
                    e.printStackTrace(System.err);
                }
            }
        });
        JToolBar bar = new JToolBar("L&F");
        bar.setLayout(new FlowLayout(FlowLayout.LEFT));
        bar.add(combo);
        return bar;
    }
}