Jörg
Jörg

Reputation: 291

Extended JMenuItem (SplitMenuItem)

Clicking on a SplitMenuItem behaves as with any other regular JMenuItem. But a SplitMenuItem has an additional down arrow at its right side, and when clicking on this arrow a JComboBox like drop down menu will appear from which one can select further actions. This functionality is normally achieved in applying a submenu. The idea for a SplitMenuItem originated from a situation where there is one action predominant in frequency and some other actions related to that main action are selected rather seldomly. With a SplitMenuItem this main action is always directly accessible, since the opening of a submenu has become unnecessary. I used code from @MadProgrammer's SplitButton and adapted it to JMenuItem, but what is still inacceptable is:

What I tried so far:

In order for the MCV to run I enclose most of the SplitMenuItem class. But apart from changing JButton to JMenuItem I in fact modified only the one parameter constructor and method showPopupMenu().

/**
 * A JMenuItem that has an additional section with an arrow icon on the right 
 * that when clicked shows a JPopupMenu that is positioned flush with the
 * menu item.
 * 
 * Credit:
 * An adaptation of SplitButton finalized by MadProgrammer and DUDSS.
 * https://stackoverflow.com/questions/36352707/actions-inside-of-another-action-like-netbeans
 * Applying code from Darryl Burke's StayOpenMenuItem.
 * https://tips4java.wordpress.com/2010/09/12/keeping-menus-open/
 *
*/

import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.beans.*;
import javax.swing.*;
import javax.swing.event.*;

public class SplitMenuItem extends JMenuItem {
    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 JPopupMenu jpopupMenu;

//  From Darryl Burke's StayOpenMenuItem.
    private static MenuElement[] path;

    {
      getModel().addChangeListener(new ChangeListener() {

        @Override
        public void stateChanged(ChangeEvent e) {
          if (getModel().isArmed() && isShowing()) {
            path = MenuSelectionManager.defaultManager().getSelectedPath();
          }
        }
      });
    }


    public SplitMenuItem(JMenu parent) {
        super();
        addMouseMotionListener(getMouseHandler());
        addMouseListener(getMouseHandler());
        // Default for no "default" action...
        setAlwaysDropDown(true);
//      The next line prevents the JMenu's item list/JPopupMenu to become
//      invisible when clicking on SplitMenuItem's arrow.
        setUI(new StayOpenMenuItemUI());

        InputMap im = getInputMap(WHEN_FOCUSED);
        im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0),
                                                        "PopupMenu.close");
        ActionMap am = getActionMap();
        am.put("PopupMenu.close", new ClosePopupAction());
        JPopupMenu parentPop= parent.getPopupMenu();
/*      Never fired.
        parentPop.addFocusListener(new FocusAdapter() {
            public void focusLost(FocusEvent e) {
                DBG.p("ParentMenu lost focus");
            }
        });
*/
        parentPop.addPopupMenuListener(new PopupMenuListener() {
            public void popupMenuCanceled(PopupMenuEvent e) {
            }

            public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
//              This method is called before any actionPerformed in child-popup.
                Timer t = new javax.swing.Timer(0, new ActionListener() {
                  public void actionPerformed(ActionEvent e) {
                    if (jpopupMenu.isVisible())
                    MenuSelectionManager.defaultManager().setSelectedPath(path);
                  }
                });
                t.setRepeats(false);
                t.start();
            }

            public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
            }
        });
    }

    public SplitMenuItem(JMenu parent, String text) {
        this(parent);
        setText(text);
    }

    public SplitMenuItem(JMenu parent, String text, JPopupMenu popup) {
        this(parent);
        setText(text);
        setPopupMenu(popup);
    }

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

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

    @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 menuItems "default" action should
        // be fired or not.  We don't want it firing if the menuItem
        // is in "options only" mode or the user clicked on the
        // "drop down arrow".
        if (onSplit || isAlwaysDropDown()) {
            showPopupMenu();
        } else {
            super.fireActionPerformed(event);
        }
    }

    /**
     * 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;
    }

    @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 MouseHandler getMouseHandler() {
        if (mouseHandler == null) {
            mouseHandler = new MouseHandler();
        }
        return mouseHandler;
    }

    protected int getOptionsCount() {
        return getPopupMenu().getComponentCount();
    }

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

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

    @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("MenuItem.background"));
                g.drawLine(1, separatorSpacing + 2, 1,
                        getHeight() - separatorSpacing - 2);
                g.setColor(UIManager.getLookAndFeelDefaults()
                        .getColor("MenuItem.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;
    }

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

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


    protected void showPopupMenu() {
        if (getOptionsCount() > 0) {
            JPopupMenu popup = getPopupMenu();
            Point p= getLocationOnScreen();
            popup.setLocation(p.x+getWidth() - popup.getPreferredSize().width,
                              p.y+getHeight());
            popup.setVisible(true);
//            Must be showing on the screen to determine its location.
//            popup.show(this, (getWidth() - popup.getWidth()), getHeight());
        }
    }

/*
    private JMenu getMenu() {
      JMenu menu = null;
      while (menu == null) {
        JPopupMenu popup = (JPopupMenu)this.getParent();
        JMenuItem item = (JMenuItem)popup.getInvoker();
        if (!(item.getParent() instanceof JPopupMenu)) menu = (JMenu)item;
      }
      return menu;
    }
*/

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

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


/**************************************************************************/
import javax.swing.*;
import javax.swing.plaf.basic.*;

class StayOpenMenuItemUI extends BasicMenuItemUI {
 
  @Override
  protected void doClick(MenuSelectionManager msm) {
    menuItem.doClick(0);
  }
}


/**************************************************************************/
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class SplitMenuItemTest extends JFrame {
  public static final long serialVersionUID = 100L;
  JMenuItem exitItem, welcomeItem;
  SplitMenuItem splitItem;

  public SplitMenuItemTest() {
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    setSize(300, 240);
    setLocationRelativeTo(null);

    JMenuBar menuBar= new JMenuBar();
    setJMenuBar(menuBar);
    JMenu menu= new JMenu("A menu");
    menuBar.add(menu);

    welcomeItem= new JMenuItem("Welcome");
    ActListener actListener= new ActListener();
    welcomeItem.addActionListener(actListener);
    menu.add(welcomeItem);

    JPopupMenu popup= createPopupForItem();
    splitItem= new SplitMenuItem(menu, "Most often this", popup);
    splitItem.addActionListener(e -> {
      JOptionPane.showMessageDialog(SplitMenuItemTest.this,
        "The usual action of this menuItem will be performed.");
    });
    menu.add(splitItem);

    exitItem= new JMenuItem("Exit");
    exitItem.addActionListener(actListener);
    menu.add(exitItem);
    setVisible(true);
  }


  static public void main(String args[]) {
    EventQueue.invokeLater(SplitMenuItemTest::new);
  }


  private JPopupMenu createPopupForItem() {
    JPopupMenu popup= new JPopupMenu();
    JMenuItem seldomItem= popup.add("Seldomly used");
    seldomItem.addActionListener(e -> {
      System.out.println(seldomItem.getText());
      popup.setVisible(false);
    });
    JMenuItem rareTaskItem= popup.add("Rare task");
    rareTaskItem.addActionListener(e -> {
      System.out.println(rareTaskItem.getText());
      popup.setVisible(false);
    });
    popup.addSeparator();
    JMenuItem cancelItem= popup.add("Cancel"); // Mandatory JMenuItem.
    cancelItem.addActionListener(e -> {
      popup.setVisible(false);
    });
    return popup;
  }


  public class ActListener implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      Object obj= e.getSource();
      if (obj==exitItem)
        System.exit(0);
      else if (obj==welcomeItem)
        System.out.println("Welcome");
      else
        System.out.println("SplitItem was clicked.");
    }
  }

}

Upvotes: 1

Views: 168

Answers (1)

queeg
queeg

Reputation: 9374

How about using a submenu with a JMenu-derived class that renders with a horizontal line (which could also be replaced by the text "More actions..." or something)?

Then your navigation problems should be gone while the look and feel would still fairly be the same.

Upvotes: 0

Related Questions