Skip to main content

Implement Shadow for JPopupMenu in Synth Look And Feel

Posted by xuanyun on March 11, 2009 at 12:50 AM PDT

In EaSynth
look and feel
, you can see the shadow effect of JPopupMenu:

jpopupmenu_shadow.png

The shadow makes the popup menu looks like a real floating object, attracts
users to put focus on it. Today let's see how to implement it in Synth look
and feel.

Please pay attention to the enlarged view of shadow area, the shadow is not
covering the background completely, it is alpha-transparent instead. So we can
consider using an alpha-transparent PNG image to make the shadow. It seems very
easy, right? We can prepare a PNG image like this:

popupmenu-background.png

This PNG image use transparent background, and the shadow on it is alpha-transparent.
If we use this image as the background of JPopupMenu, like this:

<!-- The style for popup menu. -->
<style id="Popup Menu">
  <insets top="4" left="4" bottom="10" right="9"/>    
  <imagePainter method="popupMenuBackground" path="resource/popupmenu-background.png" sourceInsets="3 3 9 9" destinationInsets="3 3 9 9" paintCenter="true" stretch="true" center="false"/>
</style>                                                
<bind style="Popup Menu" type="region" key="PopupMenu" />

Here we specify the insets to make sure the shadow area will not be occupied
by the menu items. Let's apply this style to a demo application and see the
effect:

shadow_lightpopup.png

Seems pretty good, is it so simple? Not really, if I popup the menu at another
location...

shadow_heavypopup.png

Oh... the shadow is not alpha-transparent anymore. What happened?

The magic is that when JPopupMenu is moved out of the area of its parent window,
the popup from light-weight component changed to heavy-weight component. If
the popup is heavy-weight, it could not be transparent, even you paint nothing
on it, it will have a white background, and the content under this popup will
be covered. When you try to implement a window with special shape, you will
meet the same difficulty.

Fortunately we have workaround, we can capture the image under the popup, and
paint it as the popup's background, thus the popup looks like transparent. So
the problem become "when, where and how can we capture the background image?"
We can use java.awt.Robot class to capture screen, but we could
not capture the whole screen, or the performance will be poor. In order to capture
the area under the popup, we need to implement our own popup class and do the
capture in its show() method. Here are some source code from EaSynth look and
feel:


public class EaSynthPopup extends Popup {
 
  public static final String POPUP_BACKGROUND_IMAGE = "POPUP_BACKGROUND_IMAGE";

  private Component contents;

  private int x;

  private int y;

  private Popup popup;

  private Container heavyContainer;

  private static BufferedImage popupBgImage;
 
  public EaSynthPopup(Component owner, Component contents, int x, int y, Popup popup) {
    this.contents = contents;
    this.popup = popup;
    this.x = x;
    this.y = y;
    ......
  }
 
  public Popup getPopup() {
    return this.popup;
  }

  public void show() {
    int i = ((this.contents instanceof JPopupMenu)) ? 1 : 0;
    if ((i != 0) && (this.heavyContainer == null))
      this.heavyContainer = this.contents.getParent();
    if (this.heavyContainer == null) {
      this.popup.show();
      return;
    }
   SwingUtilities.invokeLater(new CaptureBgRunnable());
  }

  private void captureBackground() {
    if (this.heavyContainer != null) {
      try {
        final Robot robot = new Robot();
        final Dimension localDimension = this.heavyContainer.getPreferredSize();
        final Rectangle localRectangle = new Rectangle(this.x, this.y, localDimension.width, localDimension.height);
        popupBgImage = robot.createScreenCapture(localRectangle);
      } catch (Exception e) {
        e.printStackTrace();
      }
    }
  }
 
  private class CaptureBgRunnable implements Runnable {
    public void run() {
      if (contents != null && contents.getParent() instanceof JComponent) {
        captureBackground();
        ((JComponent) contents.getParent()).putClientProperty(POPUP_BACKGROUND_IMAGE, popupBgImage);
      }
      final Popup popup = getPopup();
      if (popup != null) {
        popup.show();
      }
    }
  }
}

The key parts are marked with red color, when the show() method is called,
the background image of the popup will be captured and stored as a client property
of the parent of popup menu, thus we can retrive it in our painter class.

We need to implement our own painter class, or the popup background image will
not be painted automatically. Within our painter, we need to do two things,
one is retrive the background image and paint it on the popup, the other is
to paint the popupmenu "background" image with alpha-transparent shadow
(just like the imagePainter did in our first example).

In our painter class, we define the paintPopupMenuBackground() method like
this:

/**
* Paint the background of the popup menu, implement the shadow
*/

public void paintPopupMenuBackground(SynthContext context, Graphics g, int x, int y, int w, int h) {
  final JPopupMenu popupMenu = (JPopupMenu)context.getComponent();
  final JPanel panel = (JPanel)popupMenu.getParent();
  final BufferedImage bgImage = (BufferedImage)(panel.getClientProperty(EaSynthPopup.POPUP_BACKGROUND_IMAGE));
  if (bgImage != null) {
    g.drawImage(bgImage, x, y, null);
  }
  final ImageIcon bgIcon = (ImageIcon) context.getStyle().getIcon(context, "EaSynth.popup.menu.bg");
  if (bgIcon != null) {
    EaSynthGraphicsUtils.drawImageWith9Grids(g, bgIcon.getImage(),
        x, y, x + w, y + h,
        context.getStyle().getInsets(context, null), true);
  }
}

The Synth XML can be:

<style id="Popup Menu">
  <insets top="4" left="4" bottom="10" right="9"/>
  <imageIcon id="EaSynth_Popup_Menu_Bg" path="resource/PopupMenu-Background.png"/>
  <painter idref="EaSynthPainter" method="popupMenuBackground"/>
  <property key="EaSynth.popup.menu.bg" type="idref" value="EaSynth_Popup_Menu_Bg"/>
</style>
<bind style="Popup Menu" type="region" key="PopupMenu" />

We have to use the imageIcon to specify the image, since we could not use imagePainter
here, we can retrieve this image in the painter class.

In order to use our own popup class, we also need to implement a popup factory
for it:


public class EaSynthPopupFactory extends javax.swing.PopupFactory {
 
  private static EaSynthPopupFactory popupFactory = new EaSynthPopupFactory();

  private static PopupFactory backupPopupFactory;

  public static void install() {
    if (backupPopupFactory == null) {
      backupPopupFactory = getSharedInstance();
      setSharedInstance(popupFactory);
    }
  }

  public static void uninstall() {
    if (backupPopupFactory == null) {
      return;
    }
    setSharedInstance(backupPopupFactory);
    backupPopupFactory = null;
  }
 
  public Popup getPopup(Component owner, Component contents, int ownerX, int ownerY) {
    final Popup realPopup = super.getPopup(owner, contents, ownerX, ownerY);
    return new EaSynthPopup(owner, contents, ownerX, ownerY, realPopup);
  }
}

This popup factory should be installed in the constructor of painter class:

/**
* Install the popup factory to implement popup shadow
*/

public EaSynthPainter() {
  super();
  EaSynthPopupFactory.install();
}

Finally we finished all these, it is not easy actually. Let's see the final
result:

shadow_final.png

It just works fine, although we did so much, it's worth it.

PS: You can find all relatived source code in the EaSynth
L&F open-source bundle
.

Related Topics >>

Comments

Thanks XuanYun for your response! I found the root of the problem. Because I use special name binding for submenu, and set that special name at beginning of paintMenuBackground() method for the menu so that clients do not need to do so in their app. The concept is correct, but original code caused some of the submenu's names involved have not been set when the popup menu first pops up, so they are not aligned nicely. I have fixed the code and now works nicely. Thanks again for all your help while I was working in this area!

Hi yilile, I have not seen this kind of issue yet. That's odd, is it possible to be an evironment issue? Have you tried on other machine?

Hi XuanYun, For Synth L&F, I wonder if you have ever noticed an issue with menu item's misalignment , i.e. at startup, certain menu item(often a menu item with checkbox) does not align with other menu items in popup menu (usually align far left), but when mouse move over it, it would line up correctly, after that, it works fine during the session, the problem only occurs at the first time. Is that a known bug in Synth? If so, is there any way to workaround it? Sometime I workaround it by switching the position but that way does have limitations. Thanks!

Yes that's a problem, so such a menu can not be used in movie player :-). A possible solution is to use JNI, invoke native code to make the heavy-weight component become transparent. In JRE u10, we can use AWTUtilities class to do that, I love u10, but it is not widely used yet.

And what's about if the screen changes in the meanwhile? I didn't find a solution for JDK < 6 updated 10. Thanks