Adrián Castillo
Adrián Castillo

Reputation: 151

Actions inside of another action like Netbeans

I need to find the way to show several actions inside of another action like Netbeans does with Run Main Project icon.

You can see there is a default action Run Main Project and if you click in the little arrow next to the green play icon, you can select a specific action like Run .

I was checking the code of Netbeans but I can't find the code to make this in my application.

Upvotes: 4

Views: 631

Answers (2)

DUDSS
DUDSS

Reputation: 130

I too have been looking for a decent JSplitButton for a while, but everything I found as a standalone component was rather disappointing.

MadProgrammer's answer looked very promising, but it did not work all that well for me. I'm not 100% sure what it might be caused by, whether it's by design or because of the L&F I'm using but the provided component had focus issues. Specifically, the items of the popup did not show any hover indicators, indicating they weren't receiving focus. Also upon clicking an item in the popup the popup did not close itself.

Anyway, I rewrote parts of the component and made it use a JPopupMenu instead of a custom JFrame to avoid handling focus myself. The component set's the popup menu as its popup menu using the JComponent.setComponentPopupMenu() and then just invoking the popup menu upon clicking the dropdown arrow. This also makes it that it's possible to right-click the button to show the popup directly.

Component image

The popup has focus and behaves like a regular JPopupMenu, supporting adding stuff like separators and whatnot.

Note: The L&F used in the image is flatlaf


The button's default action can be set like a normal JButton using addActionListener() or using actions by calling setAction().

SplitButton btn = new SplitButton("Click me");
btn.addActionListener((e) -> {
    System.out.println("Button clicked");
});

SplitButton btn2 = new SplitButton(new AbstractAction("Click me") {
    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println("Button clicked");
    }
});

The popup menu can be created separately and then assigned to the button using setPopupMenu() or you can add individual items as actions to the menu without the need of creating it yourself using addAction and addActionAt.

JPopupMenu popup = new JPopupMenu();
popup.add(new JMenuItem("A popup option"));
popup.add(new JMenuItem("JMenuItem with icon", Icons.deleteBin));
popup.addSeparator();
btn.setPopupMenu(popup);
    
btn.addAction(new AbstractAction("Or don't", Icons.alert) {
    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println("Or don't clicked");
    }
});
btn.addAction(new AbstractAction("Click me in a different way") {
    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println("Click me in a different way clicked");
    }
});

The popup menu can be retrieved simply by using getPopupMenu().


The full code

Again, original by MadProgrammer and whoever he got it from :D

/**
 * A swing split button implementation.
 * A JButton that has an additional section with an arrow icon on the right that when clicked
 * shows a JPopupMenu that is positioned flush with the button.
 * 
 * The implementation sets the buttons pop-up menu using setComponentPopupMenu()
 * meaning that in addition to clicking the drop-down arrow, user can also right click
 * the button to open the pop-up menu.
 * 
 * Author: DUDSS - 21.02.2020
 * I modified the button to use a JPopupMenu instead of a custom JFrame to avoid hacky
 * focus workarounds and fix focus issues.
 * 
 * Credit:
 * Modified version of a split button by MadProgrammer.
 * https://stackoverflow.com/questions/36352707/actions-inside-of-another-action-like-netbeans
 * It's original author seems to be unknown.
 *
 */
public class SplitButton extends JButton {
    private int separatorSpacing = 4;
    private int splitWidth = 22;
    private int arrowSize = 8;
    private boolean onSplit;
    private Rectangle splitRectangle;
    private boolean alwaysDropDown;
    private Color arrowColor = Color.BLACK;
    private Color disabledArrowColor = Color.GRAY;
    private Image image;
    private MouseHandler mouseHandler;
    private boolean toolBarButton;
    
    private JPopupMenu jpopupMenu;

    /**
     * Creates a button with initial text and an icon.
     *
     * @param text the text of the button
     * @param icon the Icon image to display on the button
     */
    public SplitButton() {
        super();
        addMouseMotionListener(getMouseHandler());
        addMouseListener(getMouseHandler());
        // Default for no "default" action...
        setAlwaysDropDown(true);

        InputMap im = getInputMap(WHEN_FOCUSED);
        ActionMap am = getActionMap();

        im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "PopupMenu.close");
        am.put("PopupMenu.close", new ClosePopupAction());
    }
    
    public SplitButton(Action defaultAction) {
        this();
        setAction(defaultAction);
    }
    
    public SplitButton(Action defaultAction, JPopupMenu popup) {
        this();
        setAction(defaultAction);
        setPopupMenu(popup);
    }
    
    public SplitButton(Action defaultAction, Action... actions) {
        this();
        setAction(defaultAction);
        for (Action a : actions) {
            addAction(a);
        }
    }
    
    public SplitButton(String text) {
        this();
        setText(text);
    }
    
    public SplitButton(String text, Icon icon) {
        this();
        setText(text);
        setIcon(icon);
    }
    
    public SplitButton(String text, JPopupMenu popup) {
        this();
        setText(text);
        setPopupMenu(popup);
    }
    
    public SplitButton(String text, Icon icon, JPopupMenu popup) {
        this();
        setText(text);
        setIcon(icon);
        setPopupMenu(popup);
    }

    /**
     * Creates a pre-configured button suitable for being used on a JToolBar
     *
     * @param defaultAction
     * @param actions
     * @return
     */
    public static SplitButton createToolBarButton(Action defaultAction, Action... actions) {
        SplitButton btn = new SplitButton(defaultAction, actions);
        btn.configureForToolBar();
        return btn;
    }

    /**
     * Creates a pre-configured "options only" button suitable for being used on
     * a JToolBar
     *
     * @param text
     * @param icon
     * @param actions
     * @return
     */
    public static SplitButton createToolBarButton(String text, Icon icon, JPopupMenu popupMenu) {
        SplitButton btn = new SplitButton(text, icon);
        btn.setPopupMenu(popupMenu);
        btn.setToolTipText(text);
        btn.configureForToolBar();
        return btn;
    }

    @Override
    public void addActionListener(ActionListener l) {
        if (l != null) {
             setAlwaysDropDown(false);
        }
        super.addActionListener(l);
    }
    
    @Override
    public void setAction(Action a) {
        super.setAction(a);
        if (a != null) {
            setAlwaysDropDown(false);
        }
    }

    public void addActionAt(Action a, int index) {
        getPopupMenu().insert(a, index);
    }

    public void addAction(Action a) {
        getPopupMenu().add(a);
    }

    public void setPopupMenu(JPopupMenu popup) {
        jpopupMenu = popup;
        this.setComponentPopupMenu(popup);
    }

    /**
     * Returns the buttons popup menu.
     *
     * @return
     */
    public JPopupMenu getPopupMenu() {
        if (jpopupMenu == null) {
            jpopupMenu = new JPopupMenu();
        }
        return jpopupMenu;
    }

    /**
     * Used to determine if the button is begin configured for use on a tool bar
     *
     * @return
     */
    public boolean isToolBarButton() {
        return toolBarButton;
    }

    /**
     * Configures this button for use on a tool bar...
     */
    public void configureForToolBar() {
        toolBarButton = true;
        if (getIcon() != null) {
            setHideActionText(true);
        }
        setHorizontalTextPosition(JButton.CENTER);
        setVerticalTextPosition(JButton.BOTTOM);
        setFocusable(false);
    }

    protected MouseHandler getMouseHandler() {
        if (mouseHandler == null) {
            mouseHandler = new MouseHandler();
        }
        return mouseHandler;
    }
    
    protected int getOptionsCount() {
        return getPopupMenu().getComponentCount();
    }

    /*protected void addActionAt(Action action, int index) {
        if (index < 0 || index >= getOptionsCount()) {
            getPopupWindow().add(createMenuItem(action));
        } else {
            getPopupWindow().add(createMenuItem(action), index);
        }
    }*/

    /*protected void removeAction(Action action) {
        AbstractButton btn = getButtonFor(action);
        if (btn != null) {
            getPopupWindow().remove(btn);
        }
    }*/

    @Override
    public Insets getInsets() {
        Insets insets = (Insets) super.getInsets().clone();
        insets.right += splitWidth;
        return insets;
    }

    @Override
    public Insets getInsets(Insets insets) {
        Insets insets1 = getInsets();
        insets.left = insets1.left;
        insets.right = insets1.right;
        insets.bottom = insets1.bottom;
        insets.top = insets1.top;
        return insets1;
    }

    protected void closePopupMenu() {
        getPopupMenu().setVisible(false);
    }

    protected void showPopupMenu() {
        if (getOptionsCount() > 0) {
            JPopupMenu menu = getPopupMenu();
            menu.setVisible(true); //Necessary to calculate pop-up menu width the first time it's displayed.
            menu.show(this, (getWidth() - menu.getWidth()), getHeight());
        }
    }

    /**
     * Returns the separatorSpacing. Separator spacing is the space above and
     * below the separator( the line drawn when you hover your mouse over the
     * split part of the button).
     *
     * @return separatorSpacingimage = null; //to repaint the image with the new
     * size
     */
    public int getSeparatorSpacing() {
        return separatorSpacing;
    }

    /**
     * Sets the separatorSpacing.Separator spacing is the space above and below
     * the separator( the line drawn when you hover your mouse over the split
     * part of the button).
     *
     * @param spacing
     */
    public void setSeparatorSpacing(int spacing) {
        if (spacing != separatorSpacing && spacing >= 0) {
            int old = separatorSpacing;
            this.separatorSpacing = spacing;
            image = null;
            firePropertyChange("separatorSpacing", old, separatorSpacing);
            revalidate();
            repaint();
        }
    }

    /**
     * Show the dropdown menu, if attached, even if the button part is clicked.
     *
     * @return true if alwaysDropdown, false otherwise.
     */
    public boolean isAlwaysDropDown() {
        return alwaysDropDown;
    }

    /**
     * Show the dropdown menu, if attached, even if the button part is clicked.
     *
     * If true, this will prevent the button from raising any actionPerformed
     * events for itself
     *
     * @param value true to show the attached dropdown even if the button part
     * is clicked, false otherwise
     */
    public void setAlwaysDropDown(boolean value) {
        if (alwaysDropDown != value) {
            this.alwaysDropDown = value;
            firePropertyChange("alwaysDropDown", !alwaysDropDown, alwaysDropDown);
        }
    }

    /**
     * Gets the color of the arrow.
     *
     * @return arrowColor
     */
    public Color getArrowColor() {
        return arrowColor;
    }

    /**
     * Set the arrow color.
     *
     * @param color
     */
    public void setArrowColor(Color color) {
        if (arrowColor != color) {
            Color old = arrowColor;
            this.arrowColor = color;
            image = null;
            firePropertyChange("arrowColor", old, arrowColor);
            repaint();
        }
    }

    /**
     * gets the disabled arrow color
     *
     * @return disabledArrowColor color of the arrow if no popup attached.
     */
    public Color getDisabledArrowColor() {
        return disabledArrowColor;
    }

    /**
     * sets the disabled arrow color
     *
     * @param color color of the arrow if no popup attached.
     */
    public void setDisabledArrowColor(Color color) {
        if (disabledArrowColor != color) {
            Color old = disabledArrowColor;
            this.disabledArrowColor = color;
            image = null; //to repaint the image with the new color
            firePropertyChange("disabledArrowColor", old, disabledArrowColor);
        }
    }

    /**
     * Splitwidth is the width of the split part of the button.
     *
     * @return splitWidth
     */
    public int getSplitWidth() {
        return splitWidth;
    }

    /**
     * Splitwidth is the width of the split part of the button.
     *
     * @param width
     */
    public void setSplitWidth(int width) {
        if (splitWidth != width) {
            int old = splitWidth;
            this.splitWidth = width;
            firePropertyChange("splitWidth", old, splitWidth);
            revalidate();
            repaint();
        }
    }

    /**
     * gets the size of the arrow.
     *
     * @return size of the arrow
     */
    public int getArrowSize() {
        return arrowSize;
    }

    /**
     * sets the size of the arrow
     *
     * @param size
     */
    public void setArrowSize(int size) {
        if (arrowSize != size) {
            int old = arrowSize;
            this.arrowSize = size;
            image = null; //to repaint the image with the new size
            firePropertyChange("setArrowSize", old, arrowSize);
            revalidate();
            repaint();
        }
    }

    /**
     * Gets the image to be drawn in the split part. If no is set, a new image
     * is created with the triangle.
     *
     * @return image
     */
    public Image getImage() {
        if (image == null) {
            Graphics2D g = null;
            BufferedImage img = new BufferedImage(arrowSize, arrowSize, BufferedImage.TYPE_INT_RGB);
            g = (Graphics2D) img.createGraphics();
            g.setColor(Color.WHITE);
            g.fillRect(0, 0, img.getWidth(), img.getHeight());
            g.setColor(jpopupMenu != null ? arrowColor : disabledArrowColor);
            //this creates a triangle facing right >
            g.fillPolygon(new int[]{0, 0, arrowSize / 2}, new int[]{0, arrowSize, arrowSize / 2}, 3);
            g.dispose();
            //rotate it to face downwards
            img = rotate(img, 90);
            BufferedImage dimg = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_INT_ARGB);
            g = (Graphics2D) dimg.createGraphics();
            g.setComposite(AlphaComposite.Src);
            g.drawImage(img, null, 0, 0);
            g.dispose();
            for (int i = 0; i < dimg.getHeight(); i++) {
                for (int j = 0; j < dimg.getWidth(); j++) {
                    if (dimg.getRGB(j, i) == Color.WHITE.getRGB()) {
                        dimg.setRGB(j, i, 0x8F1C1C);
                    }
                }
            }

            image = Toolkit.getDefaultToolkit().createImage(dimg.getSource());
        }
        return image;
    }

    /**
     *
     * @param g
     */
    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        //Graphics gClone = g.create();//EDIT: Hervé Guillaume
        Color oldColor = g.getColor();
        splitRectangle = new Rectangle(getWidth() - splitWidth, 0, splitWidth, getHeight());
        g.translate(splitRectangle.x, splitRectangle.y);
        int mh = getHeight() / 2;
        int mw = splitWidth / 2;
        g.drawImage(getImage(), mw - arrowSize / 2, mh + 2 - arrowSize / 2, null);
        if (!alwaysDropDown) {
            if (getModel().isRollover() || isFocusable()) {
                g.setColor(UIManager.getLookAndFeelDefaults().getColor("Button.background"));
                g.drawLine(1, separatorSpacing + 2, 1, getHeight() - separatorSpacing - 2);
                g.setColor(UIManager.getLookAndFeelDefaults().getColor("Button.shadow"));
                g.drawLine(2, separatorSpacing + 2, 2, getHeight() - separatorSpacing - 2);
            }
        }
        g.setColor(oldColor);
        g.translate(-splitRectangle.x, -splitRectangle.y);
    }

    /**
     * Rotates the given image with the specified angle.
     *
     * @param img image to rotate
     * @param angle angle of rotation
     * @return rotated image
     */
    private BufferedImage rotate(BufferedImage img, int angle) {
        int w = img.getWidth();
        int h = img.getHeight();
        BufferedImage dimg = dimg = new BufferedImage(w, h, img.getType());
        Graphics2D g = dimg.createGraphics();
        g.rotate(Math.toRadians(angle), w / 2, h / 2);
        g.drawImage(img, null, 0, 0);
        return dimg;
    }

    @Override
    protected void fireActionPerformed(ActionEvent event) {
        // This is a little bit of a nasty trick.  Basically this is where
        // we try and decide if the buttons "default" action should
        // be fired or not.  We don't want it firing if the button
        // is in "options only" mode or the user clicked on
        // on the "drop down arrow"....
        if (onSplit || isAlwaysDropDown()) {
            showPopupMenu();
        } else {
            super.fireActionPerformed(event);

        }
    }

    protected class MouseHandler extends MouseAdapter {
        @Override
        public void mouseExited(MouseEvent e) {
            onSplit = false;
            repaint(splitRectangle);
        }

        @Override
        public void mouseMoved(MouseEvent e) {
            if (splitRectangle.contains(e.getPoint())) {
                onSplit = true;
            } else {
                onSplit = false;
            }
            repaint(splitRectangle);
        }
    }

    protected class ClosePopupAction extends AbstractAction {
        @Override
        public void actionPerformed(ActionEvent e) {
            closePopupMenu();
        }
    }
}

Upvotes: 4

MadProgrammer
MadProgrammer

Reputation: 347334

Ah, (one of) the holy grails of UI components, a split button. Over a number of years I've tried to find one which would perform well under multiple look and feels and failed dismally.

Many used multiple buttons or just resorted to using a JComboBox

Like many things, I stumbled across one which was pretty well done, but which I had to modify to suit my needs, unfortunately, I don't remember the original version or author, sorry. (If you believe this code is based on yours, please leave a comment with a link to the original and I will evaluate and provide appropriate credit)

SplitButton

Basically, if you click the button, it will run the "default" action (Bananas) otherwise you can select one of the sub elements and it will execute it

public class SplitButton extends JButton {

    private int separatorSpacing = 4;
    private int splitWidth = 22;
    private int arrowSize = 8;
    private boolean onSplit;
    private Rectangle splitRectangle;
    private JFrame popupMenu;
    private boolean alwaysDropDown;
    private Color arrowColor = Color.BLACK;
    private Color disabledArrowColor = Color.GRAY;
    private Image image;
    private MouseHandler mouseHandler;
    private boolean toolBarButton;

    private PopupWindowEventHandler popupWindowEventHandler;

    /**
     * Creates a button with initial text and an icon.
     *
     * @param text the text of the button
     * @param icon the Icon image to display on the button
     */
    public SplitButton() {
        super();
        addMouseMotionListener(getMouseHandler());
        addMouseListener(getMouseHandler());
        // Default for no "default" action...
        setAlwaysDropDown(true);

        InputMap im = getInputMap(WHEN_FOCUSED);
        ActionMap am = getActionMap();

        im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "PopupMenu.close");
        am.put("PopupMenu.close", new ClosePopupAction());

    }

    public SplitButton(Action defaultAction, Action... actions) {
        this();
        setAction(defaultAction);
        for (Action action : actions) {
            addAction(action);
        }
    }

    public SplitButton(String text, Icon icon, Action... actions) {
        this((Action) null, actions);
        setText(text);
        setIcon(icon);
    }

    public SplitButton(String text, Action... actions) {
        this((Action) null, actions);
        setText(text);
    }

    public JSplitButton(Icon icon, Action... actions) {
        this((Action) null, actions);
        setIcon(icon);
    }

    @Override
    public void setAction(Action a) {
        super.setAction(a);
        if (a != null) {
            setAlwaysDropDown(false);
        }
    }

    /**
     * Creates a pre-configured button suitable for being used on a JToolBar
     *
     * @param defaultAction
     * @param actions
     * @return
     */
    public static SplitButton createToolBarButton(Action defaultAction, Action... actions) {
        JSplitButton btn = new JSplitButton(defaultAction, actions);
        btn.configureForToolBar();
        return btn;
    }

    /**
     * Creates a pre-configured "options only" button suitable for being used on
     * a JToolBar
     *
     * @param text
     * @param icon
     * @param actions
     * @return
     */
    public static SplitButton createToolBarButton(String text, Icon icon, Action... actions) {
        JSplitButton btn = new JSplitButton(icon, actions);
        btn.setToolTipText(text);
        btn.configureForToolBar();
        return btn;
    }

    /**
     * Used to determine if the button is begin configured for use on a tool bar
     *
     * @return
     */
    public boolean isToolBarButton() {
        return toolBarButton;
    }

    /**
     * Configures this button for use on a tool bar...
     */
    public void configureForToolBar() {
        toolBarButton = true;
        if (getIcon() != null) {
            setHideActionText(true);
        }
        setHorizontalTextPosition(JButton.CENTER);
        setVerticalTextPosition(JButton.BOTTOM);
        setFocusable(false);
    }

    protected MouseHandler getMouseHandler() {
        if (mouseHandler == null) {
            mouseHandler = new MouseHandler();
        }
        return mouseHandler;
    }

    protected AbstractButton getButtonFor(Action action) {
        Container parent = ((JFrame) getPopupWindow()).getContentPane();
        AbstractButton btn = null;
        for (Component comp : parent.getComponents()) {
            if (comp instanceof AbstractButton) {
                Action childAction = ((AbstractButton) comp).getAction();
                if (action.equals(childAction)) {
                    btn = (AbstractButton) comp;
                    break;
                }
            }
        }

        return btn;
    }

    /**
     * Returns the index of the specified action within the popup window or -1
     * of it does not exist
     *
     * @param action
     * @return
     */
    public int indexOfAction(Action action) {
        Container parent = ((JFrame) getPopupWindow()).getContentPane();
        AbstractButton btn = getButtonFor(action);

        return btn == null ? -1 : parent.getComponentZOrder(btn);
    }

    /**
     * Adds the specified action to the popup menu...
     *
     * This simply calls getPopupWindow().add(action)
     *
     * @param action Add
     */
    public void addAction(Action action) {
        addActionAt(action, -1);
    }

    protected int getOptionsCount() {
        return ((JFrame) getPopupWindow()).getContentPane().getComponentCount();
    }

    protected void addActionAt(Action action, int index) {
        if (index < 0 || index >= getOptionsCount()) {
            getPopupWindow().add(createMenuItem(action));
        } else {
            getPopupWindow().add(createMenuItem(action), index);
        }
    }

    protected void removeAction(Action action) {
        AbstractButton btn = getButtonFor(action);
        if (btn != null) {
            getPopupWindow().remove(btn);
        }
    }

    /**
     * Creates a new JMenuItem from the supplied Action. This is used to
     * provided the ability for subclasses to either change the type of menu
     * item used by the button or add additional functionality (like listeners)
     * should they be required
     *
     * @param action
     * @return
     */
    protected JMenuItem createMenuItem(Action action) {
        return new JMenuItem(action);
    }

    @Override
    public Insets getInsets() {
        Insets insets = (Insets) super.getInsets().clone();
        insets.right += splitWidth;
        return insets;
    }

    @Override
    public Insets getInsets(Insets insets) {
        Insets insets1 = getInsets();
        insets.left = insets1.left;
        insets.right = insets1.right;
        insets.bottom = insets1.bottom;
        insets.top = insets1.top;
        return insets1;
    }

    /**
     * Returns the window that acts as the buttons popup window
     *
     * @return
     */
    public Window getPopupWindow() {
        if (popupMenu == null) {
            popupMenu = new JFrame();
            popupMenu.setFocusableWindowState(false);
            popupMenu.setUndecorated(true);
            popupMenu.setContentPane(createPopupWindowContentPane());
            popupMenu.setAlwaysOnTop(true);
            DefaultKeyboardFocusManager.getCurrentKeyboardFocusManager().addPropertyChangeListener(new PropertyChangeListener() {
                @Override
                public void propertyChange(PropertyChangeEvent evt) {
                    String name = evt.getPropertyName();
                    if ("focusOwner".equalsIgnoreCase(name)
                            || "permanentFocusOwner".equalsIgnoreCase(name)
                            || "focusedWindow".equalsIgnoreCase(name)
                            || "activeWindow".equalsIgnoreCase(name)) {
                        Window focusedWindow = DefaultKeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusedWindow();
                        if (!popupMenu.equals(focusedWindow)) {
                            closePopupWinodw();
                        }
                    }
                }
            });
        }
        return popupMenu;
    }

    protected Container createPopupWindowContentPane() {
        return new DefaultMenuPane();
    }

    protected void closePopupWinodw() {
        getPopupWindow().setVisible(false);
        if (popupWindowEventHandler != null) {
            Toolkit.getDefaultToolkit().removeAWTEventListener(popupWindowEventHandler);
        }
    }

    protected void showPopupWindow() {
        Window popup = getPopupWindow();
        popup.pack();
        Point pos = getLocationOnScreen();
        popup.setLocation(pos.x + (getWidth() - popup.getWidth()), pos.y + getHeight());
        popup.setVisible(true);

        if (popupWindowEventHandler == null) {
            popupWindowEventHandler = new PopupWindowEventHandler();
        }
        Toolkit.getDefaultToolkit().addAWTEventListener(popupWindowEventHandler, AWTEvent.MOUSE_EVENT_MASK);
    }

    /**
     * Returns the separatorSpacing. Separator spacing is the space above and
     * below the separator( the line drawn when you hover your mouse over the
     * split part of the button).
     *
     * @return separatorSpacingimage = null; //to repaint the image with the new
     * size
     */
    public int getSeparatorSpacing() {
        return separatorSpacing;
    }

    /**
     * Sets the separatorSpacing.Separator spacing is the space above and below
     * the separator( the line drawn when you hover your mouse over the split
     * part of the button).
     *
     * @param spacing
     */
    public void setSeparatorSpacing(int spacing) {
        if (spacing != separatorSpacing && spacing >= 0) {
            int old = separatorSpacing;
            this.separatorSpacing = spacing;
            image = null;
            firePropertyChange("separatorSpacing", old, separatorSpacing);
            revalidate();
            repaint();
        }
    }

    /**
     * Show the dropdown menu, if attached, even if the button part is clicked.
     *
     * @return true if alwaysDropdown, false otherwise.
     */
    public boolean isAlwaysDropDown() {
        return alwaysDropDown;
    }

    /**
     * Show the dropdown menu, if attached, even if the button part is clicked.
     *
     * If true, this will prevent the button from raising any actionPerformed
     * events for itself
     *
     * @param value true to show the attached dropdown even if the button part
     * is clicked, false otherwise
     */
    public void setAlwaysDropDown(boolean value) {
        if (alwaysDropDown != value) {
            this.alwaysDropDown = value;
            firePropertyChange("alwaysDropDown", !alwaysDropDown, alwaysDropDown);
        }
    }

    /**
     * Gets the color of the arrow.
     *
     * @return arrowColor
     */
    public Color getArrowColor() {
        return arrowColor;
    }

    /**
     * Set the arrow color.
     *
     * @param color
     */
    public void setArrowColor(Color color) {
        if (arrowColor != color) {
            Color old = arrowColor;
            this.arrowColor = color;
            image = null;
            firePropertyChange("arrowColor", old, arrowColor);
            repaint();
        }
    }

    /**
     * gets the disabled arrow color
     *
     * @return disabledArrowColor color of the arrow if no popup attached.
     */
    public Color getDisabledArrowColor() {
        return disabledArrowColor;
    }

    /**
     * sets the disabled arrow color
     *
     * @param color color of the arrow if no popup attached.
     */
    public void setDisabledArrowColor(Color color) {
        if (disabledArrowColor != color) {
            Color old = disabledArrowColor;
            this.disabledArrowColor = color;
            image = null; //to repaint the image with the new color
            firePropertyChange("disabledArrowColor", old, disabledArrowColor);
        }
    }

    /**
     * Splitwidth is the width of the split part of the button.
     *
     * @return splitWidth
     */
    public int getSplitWidth() {
        return splitWidth;
    }

    /**
     * Splitwidth is the width of the split part of the button.
     *
     * @param width
     */
    public void setSplitWidth(int width) {
        if (splitWidth != width) {
            int old = splitWidth;
            this.splitWidth = width;
            firePropertyChange("splitWidth", old, splitWidth);
            revalidate();
            repaint();
        }
    }

    /**
     * gets the size of the arrow.
     *
     * @return size of the arrow
     */
    public int getArrowSize() {
        return arrowSize;
    }

    /**
     * sets the size of the arrow
     *
     * @param size
     */
    public void setArrowSize(int size) {
        if (arrowSize != size) {
            int old = arrowSize;
            this.arrowSize = size;
            image = null; //to repaint the image with the new size
            firePropertyChange("setArrowSize", old, arrowSize);
            revalidate();
            repaint();
        }
    }

    /**
     * Gets the image to be drawn in the split part. If no is set, a new image
     * is created with the triangle.
     *
     * @return image
     */
    public Image getImage() {
        if (image == null) {
            Graphics2D g = null;
            BufferedImage img = new BufferedImage(arrowSize, arrowSize, BufferedImage.TYPE_INT_RGB);
            g = (Graphics2D) img.createGraphics();
            g.setColor(Color.WHITE);
            g.fillRect(0, 0, img.getWidth(), img.getHeight());
            g.setColor(popupMenu != null ? arrowColor : disabledArrowColor);
            //this creates a triangle facing right >
            g.fillPolygon(new int[]{0, 0, arrowSize / 2}, new int[]{0, arrowSize, arrowSize / 2}, 3);
            g.dispose();
            //rotate it to face downwards
            img = rotate(img, 90);
            BufferedImage dimg = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_INT_ARGB);
            g = (Graphics2D) dimg.createGraphics();
            g.setComposite(AlphaComposite.Src);
            g.drawImage(img, null, 0, 0);
            g.dispose();
            for (int i = 0; i < dimg.getHeight(); i++) {
                for (int j = 0; j < dimg.getWidth(); j++) {
                    if (dimg.getRGB(j, i) == Color.WHITE.getRGB()) {
                        dimg.setRGB(j, i, 0x8F1C1C);
                    }
                }
            }

            image = Toolkit.getDefaultToolkit().createImage(dimg.getSource());
        }
        return image;
    }

    /**
     *
     * @param g
     */
    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        //Graphics gClone = g.create();//EDIT: Hervé Guillaume
        Color oldColor = g.getColor();
        splitRectangle = new Rectangle(getWidth() - splitWidth, 0, splitWidth, getHeight());
        g.translate(splitRectangle.x, splitRectangle.y);
        int mh = getHeight() / 2;
        int mw = splitWidth / 2;
        g.drawImage(getImage(), mw - arrowSize / 2, mh + 2 - arrowSize / 2, null);
        if (!alwaysDropDown) {
            if (getModel().isRollover() || isFocusable()) {
                g.setColor(UIManager.getLookAndFeelDefaults().getColor("Button.background"));
                g.drawLine(1, separatorSpacing + 2, 1, getHeight() - separatorSpacing - 2);
                g.setColor(UIManager.getLookAndFeelDefaults().getColor("Button.shadow"));
                g.drawLine(2, separatorSpacing + 2, 2, getHeight() - separatorSpacing - 2);
            }
        }
        g.setColor(oldColor);
        g.translate(-splitRectangle.x, -splitRectangle.y);
    }

    /**
     * Rotates the given image with the specified angle.
     *
     * @param img image to rotate
     * @param angle angle of rotation
     * @return rotated image
     */
    private BufferedImage rotate(BufferedImage img, int angle) {
        int w = img.getWidth();
        int h = img.getHeight();
        BufferedImage dimg = dimg = new BufferedImage(w, h, img.getType());
        Graphics2D g = dimg.createGraphics();
        g.rotate(Math.toRadians(angle), w / 2, h / 2);
        g.drawImage(img, null, 0, 0);
        return dimg;
    }

    @Override
    protected void fireActionPerformed(ActionEvent event) {
        // This is a little bit of a nasty trick.  Basically this is where
        // we try and decide if the buttons "default" action should
        // be fired or not.  We don't want it firing if the button
        // is in "options only" mode or the user clicked on
        // on the "drop down arrow"....
        if (onSplit || isAlwaysDropDown()) {
            showPopupWindow();
        } else {
            super.fireActionPerformed(event);

        }
    }

    protected class MouseHandler extends MouseAdapter {

        @Override
        public void mouseExited(MouseEvent e) {
            onSplit = false;
            repaint(splitRectangle);
        }

        @Override
        public void mouseMoved(MouseEvent e) {
            if (splitRectangle.contains(e.getPoint())) {
                onSplit = true;
            } else {
                onSplit = false;
            }
            repaint(splitRectangle);
        }
    }

    protected class PopupWindowEventHandler implements AWTEventListener {

        @Override
        public void eventDispatched(AWTEvent event) {
            if (popupMenu.isVisible()) {
                switch (event.getID()) {
                    case MouseEvent.MOUSE_RELEASED:
                        Object source = event.getSource();
                        if (source instanceof Component) {
                            Window win = SwingUtilities.getWindowAncestor((Component) source);
                            if (!popupMenu.equals(win)) {
                                closePopupWinodw();
                            }
                        }
                        break;
                }
            }
        }

    }

    protected class ClosePopupAction extends AbstractAction {

        @Override
        public void actionPerformed(ActionEvent e) {
            closePopupWinodw();
        }

    }

    protected class DefaultMenuPane extends JPanel {

        public DefaultMenuPane() {
            setBorder(UIManager.getBorder("PopupMenu.border"));
            setBackground(UIManager.getColor("PopupMenu.background"));
            setLayout(new GridLayout(0, 1));
        }

    }

}

It would be configured something like ...

SplitButton btn = new SplitButton();
btn.setAction(new FruitAction("Banana", new BananaIcon(32, 32)));
btn.addAction(new FruitAction("Apple", new AppleIcon(32, 32)));
btn.addAction(new FruitAction("Black Berry", new BlackBerriesIcon(32, 32)));
btn.addAction(new FruitAction("Grapes", new GrapesIcon(32, 32)));
btn.addAction(new FruitAction("Peach", new PeachIcon(32, 32)));
btn.addAction(new FruitAction("Strewberry", new StrewberriesIcon(32, 32)));

And, for reference, the fruit action looks like...

public class FruitAction extends AbstractAction {

    public FruitAction(String text, Icon icon) {

        putValue(NAME, text);
        putValue(SMALL_ICON, icon);
        putValue(SHORT_DESCRIPTION, text);

    }

    @Override
    public void actionPerformed(ActionEvent e) {

        JOptionPane.showMessageDialog(null, "I am " + getValue(NAME), "Fruit", JOptionPane.INFORMATION_MESSAGE);

    }

}

This is use a custom vector based icon library, so obviously, I won't be including that, but it gives you an idea of how to configure it

Upvotes: 5

Related Questions