Robin Robin - 6 months ago 32
Java Question

Tooltip hidden behind JOGL GLCanvas on OS X when using non-Aqua look and feel

In the following program (which depends on JOGL), the tooltip of the

JLabel
is hidden behind the heavyweight
GLCanvas
when the tooltip 'fits' inside the
GLCanvas
.

import java.awt.*;

import javax.swing.*;
import javax.swing.plaf.nimbus.NimbusLookAndFeel;

import com.jogamp.opengl.awt.GLCanvas;

public class HeavyWeightTooltipTest {

public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
ToolTipManager.sharedInstance().setLightWeightPopupEnabled(false);
try {
UIManager.setLookAndFeel(NimbusLookAndFeel.class.getName());
} catch (Exception aE) {
aE.printStackTrace();
}
showUI();
}
});
}

private static void showUI(){
JFrame frame = new JFrame("TestFrame");

JLabel label = new JLabel("Label with tooltip");
label.setToolTipText("A very long tooltip to ensure it overlaps with the heavyweight component");
frame.add(label, BorderLayout.WEST);

GLCanvas glCanvas = new GLCanvas();
frame.add(glCanvas, BorderLayout.CENTER);

frame.setVisible(true);
frame.setSize(300,300);
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
}
}


Observations


  • It only happens when not using the Aqua look and feel. I could reproduce it with Nimbus and Metal look and feels, but not with the Aqua look and feel.

  • It does not happen when using a regular
    java.awt.Canvas
    , only with the JOGL
    GLCanvas
    (which is an extension of
    java.awt.Canvas
    )

  • The tooltip is rendered correctly when the tooltip is wider than the
    GLCanvas
    . The problem starts as soon as the tooltip fits into the
    GLCanvas
    (see screenshots at the end of the post)

  • It does not matter whether I call
    ToolTipManager.sharedInstance().setLightWeightPopupEnabled(false)
    or not. The problem is always reproducible

  • It works on Linux and Windows

  • In case it is relevant, I am using JOGL version 2.3.2 and Java version 1.8.0_65

    java version "1.8.0_65"
    Java(TM) SE Runtime Environment (build 1.8.0_65-b17)
    Java HotSpot(TM) 64-Bit Server VM (build 25.65-b01, mixed mode)



Tooltip correctly shown
Tooltip correctly shown
Tooltip hidden behind GLCanvas
Tooltip hidden behind GLCanvas

Edit: I logged this in the bug tracker of JOGL as bug 1306.

Answer

It seems that forcing the PopupFactory to use heavyweight tooltips (instead of medium weight tooltips) fixes the issue. This is non-trivial, and requires you to write your own PopupFactory or use reflection to call PopupFactory#setPopupType.

As I wasn't too keen on writing my own PopupFactory, I used reflection:

final class HeavyWeightTooltipEnforcerMac {

  private static final Object LOCK = new Object();
  private static PropertyChangeListener sUIManagerListener;

  private HeavyWeightTooltipEnforcerMac() {
  }

  /**
   * <p>
   *   Tooltips which overlap with the GLCanvas
   *   will be painted behind the heavyweight component when the bounds of the tooltip are contained
   *   in the bounds of the application.
   * </p>
   *
   * <p>
   *   In that case, {@code javax.swing.PopupFactory#MEDIUM_WEIGHT_POPUP} instances are used, and
   *   they suffer from this bug.
   *   Always using {@code javax.swing.PopupFactory#HEAVY_WEIGHT_POPUP} instances fixes the issue.
   * </p>
   *
   * <p>
   *   Note that the bug is only present when not using the Aqua look-and-feel.
   * Aqua uses its own {@code PopupFactory} which does not suffer from this.
   * </p>
   *
   */
  static void install() {
    synchronized (LOCK) {
      if (sUIManagerListener == null && isMacOS()) {
        installCustomPopupFactoryIfNeeded();
        sUIManagerListener = new LookAndFeelChangeListener();
        UIManager.addPropertyChangeListener(sUIManagerListener);
      }
    }
  }

  private static void installCustomPopupFactoryIfNeeded() {
    if (!isAquaLookAndFeel()) {
      PopupFactory.setSharedInstance(new AlwaysUseHeavyWeightPopupsFactory());
    }
  }

  private static final class LookAndFeelChangeListener implements PropertyChangeListener {
    @Override
    public void propertyChange(PropertyChangeEvent evt) {
      String propertyName = evt.getPropertyName();
      if ("lookAndFeel".equals(propertyName)) {
        installCustomPopupFactoryIfNeeded();
      }
    }
  }

  private static class AlwaysUseHeavyWeightPopupsFactory extends PopupFactory {
    private boolean couldEnforceHeavyWeightComponents = true;

    @Override
    public Popup getPopup(Component owner, Component contents, int x, int y) throws IllegalArgumentException {
      enforceHeavyWeightComponents();
      return super.getPopup(owner, contents, x, y);
    }

    private void enforceHeavyWeightComponents() {
      if (!couldEnforceHeavyWeightComponents) {
        return;
      }
      try {
        Method setPopupTypeMethod = PopupFactory.class.getDeclaredMethod("setPopupType", int.class);
        setPopupTypeMethod.setAccessible(true);
        setPopupTypeMethod.invoke(this, 2);
      } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException aE) {
        //If it fails once, it will fail every time. Do not try again
        //Consequence is that tooltips which overlap with a heavyweight component will be painted behind that component iso
        //on top of it
        couldEnforceHeavyWeightComponents = false;
      }
    }
  }
}

A similar fix can be found in the IntelliJ community edition: the LafManagerImpl class sets its own factory in the fixPopupWeight method, which enforces heavyweight popups.

Comments