// PathVisio,
// a tool for data visualization and analysis using Biological Pathways
// Copyright 2006-2011 BiGCaT Bioinformatics
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package org.pathvisio.core.gpmldiff;
import java.awt.AWTEvent;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Shape;
import java.awt.event.AWTEventListener;
import java.awt.event.MouseEvent;
import java.awt.font.FontRenderContext;
import java.awt.font.LineBreakMeasurer;
import java.awt.font.TextAttribute;
import java.awt.font.TextLayout;
import java.awt.geom.GeneralPath;
import java.awt.geom.RoundRectangle2D;
import java.text.AttributedString;
import java.util.HashMap;
import java.util.Map;
import javax.swing.JPanel;
import javax.swing.JViewport;
import javax.swing.SwingUtilities;
/**
* Derived from:
*
* GlassPane tutorial
* "A well-behaved GlassPane"
* http://weblogs.java.net/blog/alexfromsun/
*
* This is the final version of the GlassPane
* it is transparent for MouseEvents,
* and respects underneath component's cursors by default,
* it is also friendly for other users,
* if someone adds a mouseListener to this GlassPane
* or set a new cursor it will respect them
*
* @author Alexander Potochkin
*/
public class GlassPane extends JPanel implements AWTEventListener
{
/**
* Type of element modification
*/
private static enum Type
{
MODIFY,
ADD,
REMOVE
}
private final JPanel frame;
private Point mousePos = new Point();
private Type type;
private static Color baloonPaint;
// baloon margin is both the horizontal and vertical margin.
private static final int BALOON_SPACING = 50;
private static final int BALOON_MARGIN = 20;
private static final int HINT_FONT_SIZE = 11;
private static final float WAYPOINT_OFFSET = 200;
private static final int WRAP_WIDTH = 400;
private double zoomFactor = 1.0;
private boolean alignTop = true;
public void setPctZoom (double value)
{
zoomFactor = value / 100;
if (showHint)
{
repaint();
}
}
public GlassPane(JPanel frame)
{
super(null);
this.frame = frame;
setOpaque(false);
}
boolean showHint = false;
Map hint = null;
// view coordinates
double x1, y1, x2, y2;
/**
setModifyHint implies showHint (true)
*/
public void setModifyHint(Map aHint, double ax1, double ay1, double ax2, double ay2)
{
hint = aHint;
x1 = ax1;
y1 = ay1;
x2 = ax2;
y2 = ay2;
baloonPaint = Color.YELLOW;
showHint = true;
type = Type.MODIFY;
repaint();
}
/**
* implies showHint (true)
*/
public void setRemoveHint(Map aHint, double x, double y)
{
x1 = x;
y1 = y;
baloonPaint = Color.RED;
type = Type.REMOVE;
showHint = true;
hint = aHint;
repaint();
}
/**
* implies showHint (true)
*/
public void setAddHint(Map aHint, double x, double y)
{
x2 = x;
y2 = y;
baloonPaint = Color.GREEN;
type = Type.ADD;
showHint = true;
hint = aHint;
repaint();
}
private int baloonWidth = 0;
private int baloonHeight = 0;
JViewport oldView;
JViewport newView;
public void setViewPorts (JViewport o, JViewport n)
{
oldView = o;
newView = n;
}
/**
Note: don't call getHintShape() before setting baloonwidth and
baloonheight to meaningful values
*/
private Shape getHintShape()
{
Point pos = getHintPos();
Shape bg = new RoundRectangle2D.Double(
pos.getX(), pos.getY(),
baloonWidth, baloonHeight,
BALOON_MARGIN, BALOON_MARGIN
);
return bg;
}
/**
Note: don't call getHintPos() before setting baloonwidth and
baloonheight to meaningful values
*/
private Point getHintPos()
{
int xpos = (int)((getSize().getWidth() - baloonWidth) / 2);
int ypos = alignTop ? BALOON_SPACING : (int)(getSize().getHeight() - baloonHeight - BALOON_SPACING);
return new Point (xpos, ypos);
}
/**
enable showing of hint.
*/
public void clearHint()
{
hint = null;
baloonWidth = 0;
baloonHeight = 0;
showHint = false;
repaint();
}
protected void paintComponent(Graphics g)
{
if (!showHint) return;
Graphics2D g2 = (Graphics2D) g;
//g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.7f));
FontRenderContext frc = g2.getFontRenderContext();
// first determine size.
int currentTextWidth = 0;
Font f = new Font("SansSerif", Font.PLAIN, HINT_FONT_SIZE);
Font fb = new Font("SansSerif", Font.BOLD, HINT_FONT_SIZE);
// hash to store text layouts for later drawing.
Map layouts = new HashMap ();
int ypos = 0;
if (hint != null) for (Map.Entry entry : hint.entrySet())
{
// show the key as a bold, non-wrapped text.
// Only a single TextLayout needed.
TextLayout tl0 = new TextLayout (entry.getKey() + ": ", fb, frc);
int leftwidth = (int)tl0.getBounds().getWidth();
layouts.put (tl0, new Point (0, (int)(ypos + tl0.getAscent())));
// show the value as a plain, wrapped text.
// multiple TextLayouts are needed.
String text = entry.getValue();
AttributedString as = new AttributedString (text);
as.addAttribute(TextAttribute.FONT, f, 0, text.length());
// use LineBreakMeasurer to wrap the text across multiple lines.
LineBreakMeasurer lbm = new LineBreakMeasurer (as.getIterator(), frc);
while (lbm.getPosition() < text.length())
{
TextLayout tl = lbm.nextLayout (WRAP_WIDTH);
ypos += tl.getAscent();
layouts.put (tl, new Point (10 + leftwidth, ypos));
ypos += tl.getDescent() + tl.getLeading();
int width = leftwidth + (int)tl.getBounds().getWidth();
// store maximum width for calculating baloon width later.
if (width > currentTextWidth) { currentTextWidth = width; }
}
}
baloonWidth = currentTextWidth + 2 * BALOON_MARGIN;
baloonHeight = ypos + 2 * BALOON_MARGIN;
// figure out coordinates that are not in the way of the mouse.
Shape bg = getHintShape();
if (mousePos != null && bg.contains(mousePos))
{
// toggle alignTop and calculate new shape
alignTop = !alignTop;
bg = getHintShape();
}
g2.setPaint (baloonPaint);
g2.fill (bg);
g2.setColor (Color.BLACK);
g2.draw (bg);
Point hintPos = getHintPos();
// now start actual drawing of text
for (Map.Entry entry : layouts.entrySet())
{
Point textPos = entry.getValue();
TextLayout l = entry.getKey();
l.draw(
g2,
(float)(hintPos.getX() + textPos.getX() + BALOON_MARGIN),
(float)(hintPos.getY() + textPos.getY() + BALOON_MARGIN));
}
// draw lines
Shape oldClip = g2.getClip();
g2.setStroke (new BasicStroke (5));
g2.setColor (baloonPaint);
clipView (g2, oldView);
Point p;
Point s;
GeneralPath path;
if (type != Type.ADD)
{
p = relativeToView (x1, y1, oldView);
path = new GeneralPath ();
s = new Point (
(int)(hintPos.getX()),
(int)(hintPos.getY() + baloonHeight / 2)
);
path.moveTo ((float)s.getX(), (float)s.getY());
path.curveTo (
(float)(s.getX() - WAYPOINT_OFFSET),
(float)(s.getY()),
(float)(p.getX() + WAYPOINT_OFFSET),
(float)p.getY(),
(float)p.getX(),
(float)p.getY()
);
g2.draw (path);
}
g2.setClip(oldClip);
clipView (g2, newView);
if (type != Type.REMOVE)
{
p = relativeToView (x2, y2, newView);
path = new GeneralPath ();
s = new Point (
(int)(hintPos.getX() + baloonWidth),
(int)(hintPos.getY() + baloonHeight / 2)
);
path.moveTo ((float)s.getX(), (float)s.getY());
path.curveTo (
(float)s.getX() + WAYPOINT_OFFSET,
(float)s.getY(),
(float)p.getX() - WAYPOINT_OFFSET,
(float)p.getY(),
(float)p.getX(),
(float)p.getY());
g2.draw (path);
}
g2.dispose();
}
Point relativeToView (double x, double y, JViewport view)
{
// TODO: same can be achieved simple with SwingUtilities.convertRectangle
Point p = view.getLocationOnScreen();
Point p2 = getLocationOnScreen();
Point p3 = view.getViewPosition();
int rx = (int)(p.getX() - p2.getX() - p3.getX() + (x * zoomFactor));
int ry = (int)(p.getY() - p2.getY() - p3.getY() + (y * zoomFactor));
return new Point (rx, ry);
}
void clipView (Graphics2D g2d, JViewport view)
{
Point p = view.getLocationOnScreen();
Dimension d = view.getSize();
Point p2 = getLocationOnScreen();
// TODO: same can be achieved simple with SwingUtilities.convertPoint
g2d.setClip (
(int)(p.getX() - p2.getX()),
(int)(p.getY() - p2.getY()),
(int)(d.getWidth()),
(int)(d.getHeight())
);
}
/**
Test if the mouse touches last known shape location, and
repaints if so.
*/
private void testMouseCursor()
{
Shape bg = getHintShape();
if (mousePos != null && bg.contains(mousePos))
{
// toggle alignTop
alignTop = !alignTop;
repaint();
}
}
public void eventDispatched(AWTEvent event)
{
if (event instanceof MouseEvent)
{
MouseEvent me = (MouseEvent) event;
if (!SwingUtilities.isDescendingFrom(me.getComponent(), frame))
{
return;
}
if (me.getID() == MouseEvent.MOUSE_EXITED && me.getComponent() == frame)
{
mousePos = null;
}
else
{
MouseEvent converted = SwingUtilities.convertMouseEvent(me.getComponent(), me, this);
mousePos = converted.getPoint();
testMouseCursor();
}
}
}
/**
* If someone adds a mouseListener to the GlassPane or set a new cursor
* we expect that he knows what he is doing
* and return the super.contains(x, y)
* otherwise we return false to respect the cursors
* for the underneath components
*/
public boolean contains(int x, int y)
{
if (getMouseListeners().length == 0 && getMouseMotionListeners().length == 0
&& getMouseWheelListeners().length == 0
&& getCursor() == Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR))
{
return false;
}
return super.contains(x, y);
}
}