Skip to main content

Implementing [Fake] Modal Dialogs in Full-Screen Mode

Posted by mickleness on April 5, 2011 at 11:04 PM PDT

The Problem

In one of our desktop applications we're emphasizing full-screen mode more than ever before. Among other things, this means we need to display some modal dialogs in full-screen mode. On Windows this appears to work fine, but on Mac the dialog appears to be buried. Here is a test application demonstrating the problem:

import java.awt.DisplayMode;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;


public class FullScreenTest extends JFrame {
private static final long serialVersionUID = 1L;

public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
FullScreenTest test = new FullScreenTest();
test.activate(null);
}
});
}

GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice gd = env.getDefaultScreenDevice();
JButton showDialogButton = new JButton("Show Dialog");

public FullScreenTest() {
super();

showDialogButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
JDialog dialog = new JDialog( FullScreenTest.this, "Message" );
dialog.getContentPane().add(new JLabel("This is a label in a dialog."));
dialog.setModal(true);
dialog.pack();
dialog.setLocationRelativeTo(null);
dialog.setVisible(true);
}
});
getContentPane().add(showDialogButton);
}

public void activate(DisplayMode displayMode) {
if(gd.isFullScreenSupported()==false)
throw new UnsupportedOperationException();

gd.setFullScreenWindow(this);
Rectangle bounds = env.getMaximumWindowBounds();
setBounds(bounds);
}
}

On Mac the behavior is always the same: when you click the button the dialog is created, but the user doesn't see it. Clicking anywhere results in a beep, as if the toolkit realizes a modal dialog is present and you didn't click in it. (Tested against Mac 10.5.8, using Java 1.5 and 1.6).

What we decided to do instead in full-screen mode is to use JInternalFrames. Much to my relief: they look and feel a lot better than they did 5+ years ago. If anything they offer a couple of improvements over java.awt.Dialogs:

  • You can call JInternalFrame.setClosable(boolean)
  • The minimum/maximum size are actually respected, because it's defined by JComponents and not peers.

The Architecture

It's not practical to sift through the entire codebase and replace all dialogs with:

if( isFullScreenMode ) {
createInternalFrame();
} else {
createDialog();
}

Instead we created an AbstractDialog interface that can delegate to a JDialog, a JInternalFrame, or anything the future throws at us:

public interface AbstractDialog extends RootPaneContainer {
public abstract String getTitle();
public abstract void dispose();
public abstract void setResizable(boolean b);
public abstract boolean isResizable();
public abstract void setTitle(String title);
public abstract void pack();
public abstract void setVisible(boolean b);
public abstract boolean isVisible();
public abstract void setLocationRelativeTo(Component w);
}

You may want to add/remove methods to fit your needs, but this is our starting point. (For example: we assume all our AbstractDialogs are modal; you may not want that assumption.)

It so happens all these methods are already defined in the JDialog class, so a JDialog-based implementation looks like this:

public class JDialogDialog extends JDialog implements AbstractDialog {
private static final long serialVersionUID = 1L;

public JDialogDialog(Frame f) {
super(f, true);
}

public JDialogDialog(Dialog d) {
super(d, true);
}

public JDialogDialog(Window w) {
super(w, JDialog.ModalityType.TOOLKIT_MODAL);
}
}

The only method missing in JInternalFrame is setLocationRelativeTo(Component), so that class starts out like this:

public class InternalFrameDialog extends JInternalFrame implements AbstractDialog {
private static final long serialVersionUID = 1L;

public InternalFrameDialog() {}

public void setLocationRelativeTo(Component w) {
int width = getWidth();
int height = getHeight();
if(w==null) {
Dimension parentSize = getParent().getSize();
setLocation(parentSize.width/2-width/2,
parentSize.height/2-height/2);
return;
}
Point center = new Point(w.getWidth()/2, w.getHeight()/2);
SwingUtilities.convertPoint(w, center, this);
setLocation( center.x-width/2,
center.y-height/2);
}
}

The other piece that's missing is a standard way to construct either of these AbtractDialogs. You have several options here, but I went with a AbstractDialogFactory that looks like this:

public abstract class AbstractDialogFactory {
private static AbstractDialogFactory globalFactory = [create default factory]

public static void setGlobal(AbstractDialogFactory df) {
if(df==null)
df = [create default factory]
globalFactory = df;
}

public static AbstractDialogFactory getGlobal() {
return globalFactory;
}

public abstract AbstractDialog createDialog(Component owner);
}

Modal JInternalFrames

Strictly speaking: the modality is easy to achieve. What we need to do is display a JComponent underneath this JInternalFrame that consumes all mouse events:

fullScreenWindow.getLayeredPane().add(blockingComponent, JLayeredPane.PALETTE_LAYER);
fullScreenWindow.getLayeredPane().add(internalFrameDialog, JLayeredPane.MODAL_LAYER);

The tricky part here is pumping events. By "pumping events" I'm referring to how the following call should not return until the dialog is dismissed:

myDialog.setVisible(true)

Technically we could design around this blocking model: we could assign ActionListeners or Runnables to all the actionable buttons in a dialog. But our codebase is almost a decade old at this point: that much refactoring is not a good solution.

So to dig into this problem, I started with this excellent article that explains how Swing handles event pumping, among other things. Based on my understanding of the non-public EventDispatchThread, stack traces, and studying the source code for some java.awt files, my first solution looked like this:

public class EventPump {
static Object id = ...;
static Method pumpOneEventForHierarchyMethod = ...;

public static void pump(Component modalComponent) {
if(SwingUtilities.isEventDispatchThread()==false) {
while(modalComponent.isVisible()) {
Thread.wait();
}
return;
}
Thread thread = Thread.currentThread();
boolean doDispatch = true;
while (doDispatch && modalComponent.isVisible()) {
Object[] arguments = new Object[] { idObject, modalComponent};
Boolean results = (Boolean)pumpOneEventForHierarchyMethod.invoke(thread, arguments);
if (thread.isInterrupted() || !results.booleanValue()) {
doDispatch = false;
}
}
}
}

I started out with this approach because for years I've seen stack traces in the console that refer to EventDispatchThread.pumpOneEventForHierarchyMethod(...), so I assumed that was the best way to go. 

The good news is: this approach works. But the better news is: there's something out there that works much better. This article uses a much better approach to pumping events:

private synchronized void startModal () {
try {
if (SwingUtilities.isEventDispatchThread()) {
EventQueue theQueue = getToolkit().getSystemEventQueue();
while (isVisible()) {
AWTEvent event = theQueue.getNextEvent();
Object source = event.getSource();
if (event instanceof ActiveEvent) {
((ActiveEvent) event).dispatch();
} else if (source instanceof Component) {
((Component) source).dispatchEvent(event);
} else if (source instanceof MenuComponent) {
((MenuComponent) source).dispatchEvent(event);
} else {
System.err.println("Unable to dispatch: " + event);
}
}
} else {
while (isVisible()) {
wait();
}
}
} catch (InterruptedException ignored) { }
}

This approach doesn't use the dark arts of reflection, so it's guaranteed to work reliably in future Java versions, and it's less likely to throw SecurityExceptions. (Although the article's author notes there were some security issues in applets; those won't affect my desktop application.)

Note in both solutions: in my final code I changed the reference to isVisible() to isShowing(). It was easy to hide the parent full-screen window, but myInternalFrame.isVisible() would still return true: this effectively means the application is locked, because no AWTEvents are making it past the hidden dialog.

Conclusion

The AbstractDialog that ended up saving the day looks mostly like this:

public class InternalFrameDialog extends JInternalFrame implements AbstractDialog {
private static final long serialVersionUID = 1L;

final BlockingWindow blocker = new BlockingWindow();

public InternalFrameDialog() {
blocker.setVisible(false);
window.getLayeredPane().add(blocker, JLayeredPane.PALETTE_LAYER);
window.getLayeredPane().add(this, JLayeredPane.MODAL_LAYER);
}

public void setLocationRelativeTo(Component w) {
[... see above ...]
}

@Override
public void setVisible(boolean b) {
if(b) {
fixBackgroundColor(getContentPane());
((JComponent)getContentPane()).setOpaque(true);
}

Dimension d = new Dimension(window.getWidth(),window.getHeight());
if(blocker!=null) { //blocker is null during construction, and setVisible is called high up
blocker.setSize(d);
blocker.setVisible(b);
}
super.setVisible(b);
if(b && blocker!=null) {
startModal();
}
}

private Color panelBackground = UIManager.getColor("Panel.background");
private Color goodBackground = new Color(panelBackground.getRed(),
panelBackground.getGreen(),
panelBackground.getBlue());
private void fixBackgroundColor(Component c) {
//fixing a Max bug involving apple.laf.CColorPaintUIResource:
if(c.getBackground().equals(panelBackground)) {
c.setBackground(goodBackground);
}
if(c instanceof Container) {
Container container = (Container)c;
for(int a = 0; a < container.getComponentCount(); a++) {
fixBackgroundColor( container.getComponent(a) );
}
}
}
}

[I'm not going to go into detail about the background color issue; but trust me: it helped.]

It's not trivial to go through the codebase and replace every JDialog construction with AbstractDialogFactory.getGlobal().createDialog, but that's not a bad price to pay for the functionality we're getting.

All that's left to do is switch out the AbstractDialogFactory when the user enters and exits full-screen mode. The end result is the user sees what appears to be perfectly normal dialogs in full-screen mode, but the underlying toolkit knows differently.

Related Topics >>

Comments

<p>If you're using JOptionPane you can use ...

If you're using JOptionPane you can use createInternalFrame() for a modal one. It's public API but looking at that code it's only slightly less hacky than yours :). It does cover some corners you don't (frame location if won't fit on the screen, some (I think) focus handling in (private methods) Container#start/stopLWModal().