[Java] Swing Preview Popups

Sometimes in a JTabbedPane, it is nice to show a preview of the contents of tab in the background. This is similar to the windows preview that is available in a number of modern desktop environemnts (such as Windows and Gnome with Compiz). However, the preview popup is not a standard feature in Swing. Here, we look at how this feature may be added to the JTabbedPane.

The preview popup is a subclass of JPopupMenu. The technique used here is to “draw” the UI components into an image and resize it, to the appropriate size. The preview popup would then only have to display the resulting image. However, Swing does not appear to have any standard component for displaying an image (at least, I am not yet aware of one that will display the image that will be generated). A simple implementation of this component is to, simply, subclass JPanel and override its paint method to draw the image. The definition of this component:

public class ImagePanel extends JPanel
{
	private static final long serialVersionUID = 1L;
	private BufferedImage image;

	public ImagePanel()
	{
		this(null);
	}

	public ImagePanel(BufferedImage image)
	{
		this.image = image;
	}

	public void setImage(BufferedImage image)
	{
		this.image = image;
		
		// Change the preferred size to reflect the size of the image and the
		// border.
		Insets insets = getInsets();
		setPreferredSize(new Dimension(image.getWidth() + insets.left
				+ insets.right, image.getHeight() + insets.top + insets.bottom));
		
		// In case the image is already showing.
		repaint();
	}

	@Override
	public void paint(Graphics g)
	{
		super.paint(g);

		// Draw the image, if one has been set.
		if (image != null)
		{
			// Have to consider the border, if they are set. Otherwise, the
			// border would be covered up!
			Insets insets = getInsets();
			g.drawImage(image, insets.top, insets.left, null);
		}
	}
}

For simplicity, this example has restricted the type of supported Images to just BufferedImage. The getWidth and getHeight methods, as defined in the Images, require an ImageObserver to be specified. This is in case the image is still loading. While the image is loading, the width and height would not be known and the ImageObserver would be called later once they are known. In the case of a BufferedImage, the width and height are always known and, therefore, does not have this problem. However, it should be possible to modify this implementation to use any type of Images.

The popup would use this to display the generated thumbnail image. The following snippet shows how the popup is set up:

public class PreviewPopup extends JPopupMenu
{
	private static final long serialVersionUID = 1L;

	private final ImagePanel content = new ImagePanel();

	private final JLabel label = new JLabel();

	public PreviewPopup()
	{
		setLayout(new MigLayout("ins 3"));
		setOpaque(true);
		setBorder(BorderFactory.createLineBorder(Color.BLACK));
		
		add(content, "grow 0, wrap");
		add(label, "pushx, ax 50%");
		label.setHorizontalAlignment(JLabel.CENTER);
		content.setOpaque(false);
		content.setBorder(BorderFactory
				.createEtchedBorder(EtchedBorder.LOWERED));
	}

Note that the MigLayout, used on line 11, is not a standard Swing layout. You can download it from the MigLayout website.

The JLabel will used to provide the name of the component. Next, we need a way of providing a way of updating the contents of preview. This will involve creating a BufferedImage, where the UI components will be draw into:

	public void setContent(Component base)
	{
		label.setText(base.getName());

		// We need to figure out how much we need to scale the image by.
		// This factory will try to keep the width to 160px.
		Dimension size = base.getSize();
		double factor = 160.0d / size.getWidth();

		// The height of the image is set to keep the aspect ratio.
		BufferedImage image = new BufferedImage(160,
				(int) Math.round(size.height * factor),
				BufferedImage.TYPE_INT_ARGB);

		Graphics2D graphics = image.createGraphics();

		// Set the scale to obtain the appropriate image size.
		graphics.scale(factor, factor);
		
		// Extract the thumbnail image.
		base.paint(graphics);
		content.setImage(image);
	}
}

Finally, in the JTabbedPane, a MouseListener is added to show and hide the popup. The following listing shows how the component is setup and created (for brevity, the methods that actually creates content to place in the component have been omitted):

public class TabbedPane extends JTabbedPane
{
	private static final long serialVersionUID = 1L;

	private final PreviewPopup popup = new PreviewPopup();

	// This timer delays the updating and showing of the popup. This is in case
	// the user decides to move the mouse.
	private final Timer timer = new Timer(1000, new ActionListener()
	{

		@Override
		public void actionPerformed(ActionEvent e)
		{
			// This will provide the location of the tab title component.
			Rectangle bound = getBoundsAt(previewIndex);
			if (bound != null)
			{
				popup.setContent(getComponentAt(previewIndex));
				popup.show(TabbedPane.this, bound.x, bound.y + bound.height);
			}
			timer.stop();
		}
	});

	private int previewIndex = 0;

	public TabbedPane()
	{
		add(textTab());
		add(imageTab());
		add(aboutTab());

		MouseAdapter mouseListener = new MouseAdapter()
		{
			@Override
			public void mouseEntered(MouseEvent e)
			{
				super.mouseEntered(e);
			}

			@Override
			public void mouseExited(MouseEvent e)
			{
				onMouseExited(e);
			}

			@Override
			public void mouseMoved(MouseEvent e)
			{
				onMouseMoved(e);
			}
		};
		addMouseListener(mouseListener);
		addMouseMotionListener(mouseListener);
		
	}

    ...

	private void onMouseExited(MouseEvent e)
	{
		timer.stop();
		popup.setVisible(false);
	}

	private void onMouseMoved(MouseEvent e)
	{
		// Where did the mouse move to?
		int index = indexAtLocation(e.getX(), e.getY());
		if (index >= 0 && getSelectedIndex() != index)
		{
			previewIndex = index;
			timer.restart();
		}
		else
		{
			if (index == getSelectedIndex())
			{
				popup.setVisible(false);
			}
			timer.stop();
		}
	}
}

The popup should be displayed only when the mouse is over a tab that is not currently showing. The mouse listener is used to detect when this occurs. However, the listener may receive events while the user is still in the middle of moving the mouse. For this reason, the listener actually restarts Timer that will update the preview after a short period of time.

If you wish to see this in action, I’ve prepared some demostration code that you can download from here. To compile and run the demostration, you will need to add the MigLayout library. The demostration is the application that is feature in the screenshot at the top. It consists of three tabs. The first tab contains a text field. If content is placed into the field, the preview popup for that tab will be updated to include the content. The second tab uses an image taken by gnuckx and the original image can be viewed from here.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: