Reputation: 291
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
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