Search code examples
javamacosswingtooltipjogl

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.


Solution

  • 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.