/* * Copyright (c) 2007 Timothy Wall, All Rights Reserved * * This library is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published by the * Free Software Foundation; either version 2.1 of the License, or (at * your option) any later version. <p/> This library is distributed in * the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE. See the GNU Lesser General Public License for * more details. */ package com.sun.jna.examples; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.GradientPaint; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Shape; import java.awt.Window; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.geom.AffineTransform; import java.awt.geom.Area; import java.awt.geom.GeneralPath; import java.awt.geom.RoundRectangle2D; import javax.swing.Box; import javax.swing.JWindow; import javax.swing.Popup; import javax.swing.SwingUtilities; /** * Provides a popup balloon containing an arbitrary component. This provides * a form of content-specific decoration less transient than a tooltip, and less * heavyweight and more adaptable to changing content than a dedicated window. * Clients are responsible for invoking show and hide on the provided popup. */ // TODO: anchor balloon point // TODO: connect drop shadow and parent masks // TODO: proper preferred size for html // TODO: lightweight popups public class BalloonManager { // Avoid using drop shadow in some instances private static boolean useDropShadow() { return WindowUtils.isWindowAlphaSupported(); } private static class DropShadow extends JWindow { private static final float SHADOW_ALPHA = .25f; private static final float YSCALE = .80f; private static final double ANGLE = 2*Math.PI/24; private static final Point OFFSET = new Point(8, 8); private static final Color COLOR = new Color(0, 0, 0, 50); private Shape parentMask; private ComponentListener listener; public DropShadow(final Window parent, Shape mask) { super(parent); setFocusableWindowState(false); setName("###overrideRedirect###"); Point where = parent.isShowing() ? parent.getLocationOnScreen() : parent.getLocation(); setLocation(where.x + OFFSET.x, where.y + OFFSET.y); setBackground(COLOR); getContentPane().setBackground(COLOR); parentMask = mask; parent.addComponentListener(listener = new ComponentAdapter() { public void componentMoved(ComponentEvent e) { Point where = getOwner().isShowing() ? getOwner().getLocationOnScreen() : getOwner().getLocation(); setLocation(where.x + OFFSET.x, where.y + OFFSET.y); } public void componentResized(ComponentEvent e) { Component c = e.getComponent(); int extra = c.getWidth() + (int)Math.sin(ANGLE)*c.getHeight(); setSize(c.getWidth() + extra, c.getHeight()); WindowUtils.setWindowMask(DropShadow.this, getMask()); } public void componentShown(ComponentEvent e) { if (!isVisible()) { pack(); setVisible(true); } } }); addWindowListener(new WindowAdapter() { public void windowClosed(WindowEvent e) { if (listener != null) { parent.removeComponentListener(listener); listener = null; } } }); WindowUtils.setWindowMask(DropShadow.this, getMask()); WindowUtils.setWindowAlpha(DropShadow.this, SHADOW_ALPHA); if (parent.isVisible()) { pack(); setVisible(true); } } public void paint(Graphics graphics) { Graphics2D g = (Graphics2D)graphics.create(); // Workaround for OSX since we only get automatic clipping // on the content pane and below g.setClip(getMask()); g.setPaint(new GradientPaint(0, getHeight()/2, new Color(0,0,0,0), getWidth(), getHeight()/2, new Color(0,0,0,255))); g.fillRect(0, 0, getWidth(), getHeight()); g.dispose(); } public Dimension getPreferredSize() { Dimension size = getOwner().getPreferredSize(); size.width += 100; size.height += 100; return size; } private Shape getMask() { Area area = new Area(parentMask); Area clip = new Area(parentMask); AffineTransform tx = new AffineTransform(); tx.translate(Math.sin(ANGLE)*getOwner().getHeight(), 0); tx.shear(-Math.tan(ANGLE), 0); tx.scale(1, YSCALE); tx.translate(0, (1-YSCALE)*getOwner().getHeight()); area.transform(tx); tx = new AffineTransform(); tx.translate(-OFFSET.x, -OFFSET.y); clip.transform(tx); area.subtract(clip); return area; } } private static final class BubbleWindow extends JWindow { private static final int Y_OFFSET = 50; private static final int ARC = 25; private Point offset; private Area mask; private Dimension maskSize; private ComponentListener moveTracker = new ComponentAdapter() { public void componentMoved(ComponentEvent e) { Point where = e.getComponent().isShowing() ? e.getComponent().getLocationOnScreen() : e.getComponent().getLocation(); setLocation(where.x - offset.x, where.y - offset.y); // TODO preserve stacking order (linux) } }; public BubbleWindow(Window owner, Component content) { super(owner); setFocusableWindowState(false); setName("###overrideRedirect###"); getContentPane().setBackground(Color.white); getContentPane().add(content, BorderLayout.CENTER); getContentPane().add(Box.createVerticalStrut(Y_OFFSET), BorderLayout.SOUTH); owner.addComponentListener(moveTracker); setSize(getPreferredSize()); mask = new Area(getMask(getWidth(), getHeight())); maskSize = getSize(); WindowUtils.setWindowMask(BubbleWindow.this, mask); if (useDropShadow()) { new DropShadow(this, mask); } } public void setBounds(int x, int y, int w, int h) { super.setBounds(x, y, w, h); Dimension size = new Dimension(w, h); if (mask != null && !size.equals(maskSize)) { mask.subtract(mask); mask.add(new Area(getMask(w, h))); maskSize = size; } } public void setAnchorLocation(int x, int y) { super.setLocation(x, y); Window owner = getOwner(); if (owner != null) { Point ref = owner.isShowing() ? owner.getLocationOnScreen() : owner.getLocation(); offset = new Point(ref.x - x, ref.y - y); } } public void dispose() { super.dispose(); getOwner().removeComponentListener(moveTracker); } private Shape getMask(int w, int h) { Shape shape = new RoundRectangle2D.Float(0, 0, w, h-Y_OFFSET, ARC, ARC); Area area = new Area(shape); GeneralPath path = new GeneralPath(); path.moveTo(w/3, h-1); path.lineTo(w/2, h-1-Y_OFFSET); path.lineTo(w*2/3, h-1-Y_OFFSET); path.closePath(); area.add(new Area(path)); return area; } public Dimension getPreferredSize() { Dimension size = super.getPreferredSize(); size.height += Y_OFFSET; return size; } } /** Get a balloon pointing to the given location. The coordinates are * relative to <code>owner</code>, which if null, indicates the coordinates * are absolute. */ public static Popup getBalloon(final Component owner, final Component content, int x, int y) { // Simulate PopupFactory, ensuring we get a heavyweight "popup" final Point origin = owner == null ? new Point(0, 0) : (owner.isShowing() ? owner.getLocationOnScreen() : owner.getLocation()); final Window parent = owner != null ? SwingUtilities.getWindowAncestor(owner) : null; origin.translate(x, y); return new Popup() { private BubbleWindow w; public void show() { w = new BubbleWindow(parent, content); w.pack(); Point where = new Point(origin); where.translate(-w.getWidth()/3, -w.getHeight()); w.setAnchorLocation(where.x, where.y); w.setVisible(true); } public void hide() { w.setVisible(false); w.dispose(); } }; } }