Monday, July 1, 2013

ComboBoxUIs and PopupFactories

Introduction

I recently wanted to experiment with creating my own ComboBoxUI. Using the FilledButtonUI model as a starting point: I created the FilledComboBoxUI. (See the demo here.) It wasn't as straight-forward as I'd expected it to be: but the current draft weighs in at only 500 lines of code.

Unfortunately this highlighted a bug regarding applets in many browsers: JPopupMenus would be positioned relative to the top-left corner of the display, and not the applet. The result is that you would click a combobox and the popup menu would appear several pixels above and to the left. The same problem was also observed for tooltips.

To the right is a screenshot of this bug in the Wordle applet: although I clicked the "Font" menu, the popup that displayed is several pixels away from where it should be.

Solution

After a couple of hours of experimentation: I gave up trying to reposition the heavyweight popup.

Instead: as a work-around I decided to try displaying the popup as a lightweight component. It's not very common that we need to use the powerful complexity of the JLayeredPane: but it has the potential to display JComponent above everything (it even has a layer specifically designated for popups).

Originally my goal was only to implement a lightweight model for my new FilledComboBoxUI class (because I had such explicit control over how the popup would be invoked), but as I rummaged around I realized the javax.swing.PopupFactory is an existing architecture that may let me intercept all popups (including tooltips).

The final result is the new AppletPopupFactory (source here). After you invoke the static initialize() method: it will handle all popups. If a popup is requested outside of an applet: then the original PopupFactory is used instead (so if it's invoked in an application vs an applet: then the fancy new code isn't touched).

Additional problems included:

  • Originally for the new PopupFactory I tried calling JPopupMenu.setVisible(..) to help control visibility, because it is still a JComponent. Unfortunately: this is a complex invocation that ultimately defers to the current PopupFactory. The result (if I try to use setVisible(..)) is a recursive loop that never actually alters visibility. The solution was to simply avoid interacting with the visibility. (The JPopupMenu is a strange creature, and now I know it can function as a normal lightweight JComponent as long as you don't touch its visibility.) Instead: adding it and removing it from the parent JLayeredPane can achieve the necessary effect.

  • Clicking outside the popup needs to dismiss the popup. So behind the popup: we install a giant invisible panel that consumes MouseEvents. This means we notice when the user clicks in this area (so we can hide the popup), and we prevent the user from clicking any other component while the popup is visible.

  • The popup generally rendered correctly, but it needed decoration. By "decoration" I mean: a border and a shadow. The shadow is more of an indulgence, but I'll explain it first: since we already have a giant panel used to intercept MouseEvents: I tweaked this so it rendered a small shadow around the popup. The border proved a stranger problem: when the AquaComboBoxUI was being used on Mac 10.7.5 (Java 1.7): the border was never rendered. It was correctly defined as a simple LineBorder -- and the same LineBorder rendered correctly with the FilledComboBoxUI -- but with the Aqua model it never appeared. The (strange) work-around here was to remove the border and render it along with the shadow in the background pane. This is an unusual separation of a component from its border: but it should be visually indistinguishable for the user.

  • Because this popup exists only within the applet: it is constrained to the bounds of the applet. The original heavyweight model could appear anywhere on the monitor. (In fact: that was the original bug I'm trying to resolve -- it was appearing too far away.) So in this implementation we have to reposition the popup if it runs into the edge of the applet. In my experience this is not a major inconvenience to the user if the applet is large enough, but if you are working with a very small applet it might feel strange.
  • Conclusion

    I expect to further tweak the FilledComboBoxUI in coming weeks as I try to further improve it, but overall everything is shaping up well. There is no applet accompanying this article, but you can see these changes in action in this applet by interacting with the tooltips and comboboxes.

    This article focuses specifically on the PopupFactory and creating a lightweight alternative: and that effort appears to be finished.

    As a sidenote: you could also argue, "Who cares about applets?" The primary bug this article addresses only occurred in applets, but not in applications: so why fuss about applets? I attended a talk last week led by Roger Brinkley titled "Java Platform Now and the Future", and it seems safe to say the future of Java UI development is (at best) concentrated towards JavaFX. But this (my java.net repository) is not a business -- it's a hobby. And for the time being I'm not giving up on Swing quite yet. And in the mean time: this bug affected several of my articles/applets. So for those users brave enough to click through the security warnings and actually run my applets: I wanted to at least give them a decent experience.