1

I have a JButton in which I overrided the paintComponent(Graphics) function with a child JLabel (I realize this sounds stupid, I promise it's not) I have mouseEntered(MouseEvent) & mouseExited(MouseEvent) functions which change the visiblity of the label as well as set a boolean telling paintComponent to draw a translucent overlay over the button

The expected behaviour is that the JLabel draw over the button overlay. Without the overlay (override of paintComponent) this works perfectly.

(I'm assuming this isn't only limited to buttons, though I haven't tested that theory)

Button Class:

import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
public class HoverButton extends JButton {
 private final JLabel label;
 private final String title;
 private boolean entered = false;
 public HoverButton(String title) {
 label = new JLabel(title);
 this.title = title;
 int startChar = title.indexOf(']') + 1;
 String regex = new StringBuilder("\\[[a-zA-Z0-9]+\\]").append("| \\[[A-Za-z0-9]+ [A-Za-z0-9]+\\]")
 .append("| \\(decen\\)").append("| \\(eng, decen\\)").append("| \\(eng\\)")
 .append("|\\{.+\\}").toString();
 String text = String.format("<html><p><b>%s</b></p></html>",
 title.substring(startChar).replaceAll(regex, "").trim());
 label.setVisible(false);
 add(label);
 addMouseListener(new MouseAdapter() {
 @Override
 public void mouseEntered(MouseEvent e) {
 super.mouseEntered(e);
 entered = true;
 label.setVisible(true);
 }
 @Override
 public void mouseExited(MouseEvent e) {
 super.mouseExited(e);
 entered = false;
 label.setVisible(false);
 }
 });
 }
 public String getTitle() {
 return title;
 }
 @Override
 public void paintComponent(Graphics g) {
 super.paintComponent(g);
 if (g instanceof Graphics2D g2d) {
 getIcon().paintIcon(this, g, getInsets().left, getInsets().top);
 int xMax = getWidth() - getInsets().right - getInsets().left;
 int yMax = getHeight() - getInsets().top - getInsets().bottom;
 if (entered) {
 g2d.setColor(new Color(0x88000000, true));
 g2d.fillRect(getInsets().left, getInsets().top, xMax, yMax);
 }
 g2d.dispose();
 }
 }
 @Override
 public void setPreferredSize(Dimension d) {
 super.setPreferredSize(new Dimension((int) (d.getWidth() + getInsets().right + getInsets().left),
 (int) (d.getHeight() + getInsets().top + getInsets().bottom)));
 label.setMinimumSize(d);
 }
}

the setPreferredSize(Dimension) at the bottom is there to ensure the label is doesn't resize the button

 button.setIcon(icon);
 button.setPreferredSize(new Dimension(icon.getWidth(), icon.getHeight()));

should be in the calling class

asked Dec 12, 2024 at 16:39
11
  • 1
    probably not solving the problem, but I would start removing g2d.dispose(); - g2d was not created by posted method, should not be discarded by it (I would also stop using StringBuilder to concatenate strings, at least three reasons not do so here [no loop, only literals, no advantage after Java 9] ) Commented Dec 12, 2024 at 17:04
  • 1
    @user85421 in fact, even before Java 9 there’s no advantage. Using + for constant strings would produce a constant string in all versions whereas using + for nonconstant strings before Java 9 would produce the same compiled code as this StringBuilder code. Commented Dec 12, 2024 at 18:11
  • 1
    @user85421 well, if there were loops, I’d be careful even with Java 9. But we agree that the use of StringBuilder is not useful here. The use of String.format("<html><p><b>%s</b></p></html>", ...) to basically concatenate three parts is also inferior to just using string concatenation but also inconsistent. It looks like an attempt to use as many different ways to concatenate strings as possible. Commented Dec 12, 2024 at 18:29
  • 3
    Regarding the actual problem (or rather the apparently intended goal): JButton has built-in rollover support, so there’s no need to do all this stuff. For example: JButton b = new JButton("Some test text"); b.setRolloverEnabled(true); b.getModel().addChangeListener(ev -> b.setText(((ButtonModel)ev.getSource()).isRollover()? "<html><b>Some test text": "Some test text")); I wouldn’t mess around with the color but rather select a Look&Feel which provides good colors for roll-over highlighting. Otherwise, a custom icon would be enough to paint a translucent overlay. Commented Dec 12, 2024 at 18:34
  • 2
    @camickr actually, AbstractButton installs an OverlayLayout when you add the first child and no layout has been set so before. But there’s no sense in adding a JLabel anyway when you can simply set the text on the button in the first place, as both have the same text rendering features. Commented Dec 12, 2024 at 18:47

1 Answer 1

2

You can use a JLayer for this type of overlay painting, where the events will pass through the label.

The JLayer has two components:

  1. The view. This is the component which is wrapped by the JLayer. The JLayer will forward its events to the view. For example in this case we would like the events to pass to the button.
  2. The glass pane. This can be used as a painting area. It is a JPanel and you can use it like any other. Events will pass through it and its descendants. For example in this case we could add the label to the glass pane (JLabels can accept an Icon along with text, or an Icon by itself).

This way you don't have to do the following:

  1. Track mouse events. This is already implemented in the button, so you can directly listen for such events (via the ButtonModel).
  2. Paint the Icon. The label will do that for you.
  3. Override preferred size. The JLayer will handle this.
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Rectangle;
import javax.swing.BorderFactory;
import javax.swing.ButtonModel;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JLayer;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class MainWithJLayer {
 
 /**
 * Changes the alpha component of the given {@code Color}.
 * @param c
 * @param alpha
 * @return
 */
 public static Color withAlpha(final Color c,
 final int alpha) {
 return new Color(c.getRed(), c.getGreen(), c.getBlue(), alpha);
 }
 
 /** A {@code JPanel} which always draws its background color (dishonoring opaque property). */
 private static class AlwaysDrawBackgroundPanel extends JPanel {
 @Override
 protected void paintComponent(final Graphics g) {
 final Color originalColor = g.getColor();
 try {
 final Rectangle clipBounds = g.getClipBounds();
 g.setColor(getBackground());
 g.fillRect(clipBounds.x, clipBounds.y, clipBounds.width, clipBounds.height);
 }
 finally {
 g.setColor(originalColor);
 super.paintComponent(g);
 }
 }
 }
 
 public static void main(final String[] args) {
 SwingUtilities.invokeLater(() -> {
 
// try {
// UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
// }
// catch (final ClassNotFoundException | IllegalAccessException | InstantiationException | UnsupportedLookAndFeelException exception) {
// System.err.println("Failed to set system L&F.");
// }
 
 final JButton button = new JButton("always clickable... ...always clickable");
 button.addActionListener(e -> System.out.println("Clicked!"));
 
 final JLabel label = new JLabel("Label overlay!", JLabel.CENTER);
 label.setForeground(Color.RED);
 
 final JPanel glassPane = new AlwaysDrawBackgroundPanel();
 glassPane.setLayout(new BorderLayout());
 glassPane.setBackground(withAlpha(Color.BLACK, 0x88)); //new Color(0x88000000,true)
 glassPane.add(label, BorderLayout.CENTER);
 
 final JLayer<JButton> layer = new JLayer<>(button);
 layer.setGlassPane(glassPane);
 glassPane.setOpaque(false); //This is mandatory in order to show the button under the label.
 
 final JPanel contents = new JPanel(new BorderLayout());
 contents.setBorder(BorderFactory.createEmptyBorder(100, 100, 100, 100));
 contents.add(layer, BorderLayout.CENTER);
 
 //Change glass pane visibility when hovering the button:
 final ButtonModel buttonModel = button.getModel();
 buttonModel.addChangeListener(e -> glassPane.setVisible(buttonModel.isRollover()));
 
 final JFrame frame = new JFrame("Button overlay label");
 frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 frame.add(contents);
 frame.pack();
 frame.setLocationRelativeTo(null);
 frame.setVisible(true);
 });
 }
}

Notice that the glass pane (which has the label) is only visible when we hover the button and that the button receives events normally (with or without the label being visible).

There are also several properties of the button and the label (such as margin, border, alignment, text position, icon-text-gap) to help solve icon (and/or text) placement issues between the two components.

Note, the main possible contribution here (assuming it fits your needs) is the suggestion to use a JLayer, since using the ButtonModel's rollover property was already suggested by @Holger.

answered Dec 12, 2024 at 19:35
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.