/*
* Copyright 2011 Luke Usherwood.
*
* This program 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.
*
* This program 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.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package net.bettyluke.tracinstant.ui;
import java.awt.Color;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JInternalFrame;
import javax.swing.JLayeredPane;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.plaf.InternalFrameUI;
import javax.swing.plaf.basic.BasicInternalFrameUI;
import javax.swing.text.JTextComponent;
public class CalloutOverlay {
private static final Color SHADOW_COLOR = new Color(0, 0, 0, 30);
public static class TrianglePanel extends JPanel {
final int[] xPoints;
final int[] yPoints;
public TrianglePanel(int size) {
setOpaque(false);
xPoints = new int[] { size, size * 2, 0 };
yPoints = new int[] { 0, size, size };
setPreferredSize(new Dimension(size * 2, size));
}
@Override
public void paintComponent(Graphics g) {
Color oldColor = g.getColor();
g.setColor(getBackground());
g.fillPolygon(xPoints, yPoints, xPoints.length);
g.setColor(getForeground());
g.drawLine(xPoints[0], yPoints[0], xPoints[1], yPoints[1]);
g.drawLine(xPoints[0], yPoints[0], xPoints[2], yPoints[2]);
g.setColor(oldColor);
}
public void locatePointAt(int x, int y) {
int size = getPreferredSize().height;
setBounds(x - size, y, size * 2, size);
}
}
/**
* A poor-man's drop-shadow. TODO: Add blur.
*/
public class ShadowPanel extends JPanel {
public ShadowPanel(JComponent comp) {
comp.addComponentListener(new ComponentAdapter() {
@Override
public void componentMoved(ComponentEvent e) {
setNewBounds(e.getComponent().getBounds());
}
@Override
public void componentResized(ComponentEvent e) {
setNewBounds(e.getComponent().getBounds());
}
});
setOpaque(true);
setBackground(SHADOW_COLOR);
setNewBounds(comp.getBounds());
}
private void setNewBounds(Rectangle bounds) {
bounds.translate(3, 3);
setBounds(bounds);
}
}
public static class UntitledFrame extends JInternalFrame {
public UntitledFrame(JComponent content) {
super("");
add(content);
// Hack away the title-bar
InternalFrameUI ifu = getUI();
if (ifu instanceof BasicInternalFrameUI) {
((BasicInternalFrameUI) ifu).setNorthPane(null);
}
}
}
private final class MyMouseListener implements MouseListener, MouseMotionListener {
@Override
public void mouseClicked(MouseEvent e) {
redispatchToCallout(e);
}
@Override
public void mousePressed(MouseEvent e) {
if (!redispatchToCallout(e)) {
if (dismissListener != null) {
ActionEvent evt = new ActionEvent(this, 0, "Dismiss callout");
dismissListener.actionPerformed(evt);
}
e.consume();
}
}
@Override
public void mouseReleased(MouseEvent e) {
redispatchToCallout(e);
}
@Override
public void mouseEntered(MouseEvent e) {
redispatchToCallout(e);
}
@Override
public void mouseExited(MouseEvent e) {
redispatchToCallout(e);
}
@Override
public void mouseDragged(MouseEvent e) {
redispatchToCallout(e);
}
@Override
public void mouseMoved(MouseEvent e) {
redispatchToCallout(e);
}
private boolean redispatchToCallout(MouseEvent e) {
if (iFrame.getBounds().contains(e.getPoint())) {
redispatchMouseEvent(e);
return true;
}
return false;
}
private void redispatchMouseEvent(MouseEvent e) {
Point glassPanePoint = e.getPoint();
Point layeredPanePoint = SwingUtilities.convertPoint(glassPane,
glassPanePoint, layeredPane);
if (layeredPanePoint.y >= 0) {
// The mouse event is probably over the content pane.
// Find out exactly which component it's over.
Component component = SwingUtilities.getDeepestComponentAt(
layeredPane, layeredPanePoint.x, layeredPanePoint.y);
if (component != null) {
Point componentPoint = SwingUtilities.convertPoint(
glassPane, glassPanePoint, component);
component.dispatchEvent(new MouseEvent(component, e
.getID(), e.getWhen(), e.getModifiers(),
componentPoint.x, componentPoint.y,
e.getClickCount(), e.isPopupTrigger()));
}
// HACK! Since this code can't correctly generate mouse-entered and
// mouse-exit events for Components placed in the glass pane, we
// take over the job of changing to a Text cursor, very crudely.
glassPane.setCursor((component instanceof JTextComponent) ?
Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR) : null);
}
}
}
private final JLayeredPane layeredPane;
private final TrianglePanel triangle;
private final JInternalFrame iFrame;
private final JPanel shad;
private final Component glassPane;
private final MyMouseListener mouseListener = new MyMouseListener();
private ActionListener dismissListener;
public CalloutOverlay(JFrame frame, JComponent content) {
iFrame = new UntitledFrame(content);
shad = new ShadowPanel(iFrame);
layeredPane = frame.getLayeredPane();
glassPane = frame.getGlassPane();
triangle = new TrianglePanel(15);
triangle.setBackground(content.getBackground());
}
public void showAt(int x, int y) {
triangle.locatePointAt(x, y);
iFrame.pack();
iFrame.setLocation(
x - iFrame.getWidth() + triangle.getWidth(),
y + triangle.getHeight() - 1);
layeredPane.add(triangle, Integer.valueOf(JLayeredPane.POPUP_LAYER + 1));
layeredPane.add(iFrame, Integer.valueOf(JLayeredPane.POPUP_LAYER));
layeredPane.add(shad, Integer.valueOf(JLayeredPane.POPUP_LAYER - 1));
glassPane.setVisible(true);
glassPane.addMouseListener(mouseListener);
glassPane.addMouseMotionListener(mouseListener);
iFrame.show();
}
public Component getContent() {
return iFrame.getContentPane().getComponent(0);
}
public void dismiss() {
layeredPane.remove(triangle);
layeredPane.remove(iFrame);
layeredPane.remove(shad);
glassPane.removeMouseListener(mouseListener);
glassPane.removeMouseMotionListener(mouseListener);
glassPane.setVisible(false);
iFrame.dispose();
// TODO: is there a way to ensure the cursor updates appropriately
// to the component under the glass, before the mouse moves?
}
public void addDismissListener(ActionListener listener) {
if (dismissListener != null) {
throw new IllegalStateException();
}
dismissListener = listener;
}
}