/**
* Copyright (c) 2014 Matthias Jaenicke <matthias.jaenicke@student.kit.edu>,
* Matthias Plappert <undkc@student.kit.edu>,
* Julien Duman <uncyc@student.kit.edu>,
* Christian Dreher <uaeef@student.kit.edu>,
* Wasilij Beskorovajnov <uajkm@student.kit.edu> and
* Aydin Tekin <aydin.tekin@student.kit.edu>
*
* Released under the MIT license (refer to LICENSE.md)
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package edu.kit.iks.CryptographicsLib;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.geom.Area;
import java.awt.geom.GeneralPath;
import java.awt.geom.RoundRectangle2D;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.border.Border;
/**
* An instance of this class represents the view of a popover
*
* @author Christian Dreher
*/
public class PopoverView extends JPanel {
/**
* Defines the possible locations of a popover arrow.
*/
private enum ArrowLocation {
TOP, BOTTOM
};
/**
* The width of the arrow.
*/
final private static int ARROW_WIDTH = 40;
/**
* The height of the arrow.
*/
final private static int ARROW_HEIGHT = 20;
/**
* The inner inset of the popover.
*/
final private static int INSET = 20;
/**
* The corner radius of the popover.
*/
final private static int CORNER_RADIUS = 20;
/**
* The minimum padding between the container and the popover.
*/
final private static int MINIMUM_CONTAINER_PADDING = 5;
private Color borderColor;
/**
* The arrow location of the popover.
*/
private ArrowLocation arrowLocation = ArrowLocation.TOP;
/**
* Serial Version UID
*/
private static final long serialVersionUID = -5949014143695648861L;
/**
* Button to close the popover
*/
private JButton closeButton;
/**
* Content area of the popover
*/
private JPanel contentView;
private Point anchorPoint;
/**
* The shared container view
*/
static private JPanel containerView;
/**
* Constructor initializing a new instance of {PopoverView}
*/
public PopoverView() {
super(new GridBagLayout());
this.setOpaque(false);
this.setMaximumSize(new Dimension(500, 500));
this.setBackground(new Color(250, 250, 250, 230));
this.setBorderColor(Color.LIGHT_GRAY);
// Create close button.
GridBagConstraints closeConstraints = new GridBagConstraints();
closeConstraints.gridx = 0;
closeConstraints.gridy = 0;
closeConstraints.insets = new Insets(0, 0, 0, 0);
// \u00D7 is the unicode for the times "x" (prettier close icon as just x)
this.closeButton = new JButton("\u00D7");
// Name to enable applying custom style with synth
this.closeButton.setName("closeButton");
this.add(this.closeButton, closeConstraints);
// Create content view.
GridBagConstraints contentConstraints = new GridBagConstraints();
contentConstraints.gridx = 0;
contentConstraints.gridy = 1;
contentConstraints.gridwidth = 3;
contentConstraints.insets = new Insets(0, INSET, INSET, INSET);
this.contentView = new JPanel();
this.contentView.setOpaque(false);
this.add(this.contentView, contentConstraints);
this.validate();
}
/**
* Returns the CloseButton.
*
* @return the closeButton button to return.
*/
public JButton getCloseButton() {
return this.closeButton;
}
/**
* The content view contains the popover's content. This makes it easier to
* layout the popover and the content properly.
*
* @return the content view.
*/
public JPanel getContentView() {
return this.contentView;
}
/**
* Configures all popovers to use a given container view. The container view
* should be visible above all other components and should have maximum height
* and width. You cannot use popover views before configuring them without a container!
*
* @param containerView the container view
*/
public static void setContainerView(JPanel containerView) {
PopoverView.containerView = containerView;
// Configure container.
containerView.setLayout(null);
containerView.setVisible(true);
}
/**
* Presents a popover in the container
*
* @param originComponent the origin of the popover is where the arrow of the popover should point to
*/
public void present(JComponent anchorComponent) {
this.validate();
// Calculate the anchorPoint. The origin is in the center of the component.
double x = anchorComponent.getLocationOnScreen().getX() - PopoverView.containerView.getLocationOnScreen().getX();
x += anchorComponent.getSize().getWidth() / 2;
double y = anchorComponent.getLocationOnScreen().getY() - PopoverView.containerView.getLocationOnScreen().getY();
y += anchorComponent.getSize().getHeight() / 2;
this.anchorPoint = new Point((int)x, (int)y);
// Position in container view.
PopoverView.containerView.add(this);
PopoverView.containerView.validate();
this.prepareForPresentation();
}
/**
* Removes a popover from the container
*/
public void dismiss() {
PopoverView.containerView.remove(this);
PopoverView.containerView.revalidate();
PopoverView.containerView.repaint();
}
/**
* Paints the popover.
*/
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
final Area shape = this.createShape();
Graphics2D g2d = (Graphics2D)g;
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_OFF);
// Fill.
g2d.setPaint(this.getBackground());
g2d.fill(shape);
// Stroke.
g2d.setStroke(new BasicStroke(1.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
g2d.setPaint(this.getBorderColor());
g2d.draw(shape);
}
/**
* Creates the popover shape
* @return The popover shape
*/
private Area createShape() {
final Insets insets = new Insets(1, 1, 1, 1);
Rectangle bounds = this.getBounds();
bounds.x += insets.left;
bounds.y += insets.top;
bounds.width -= insets.left + insets.right;
bounds.height -= insets.top + insets.bottom;
// Create the basic rounded shape.
Area shape = null;
switch (this.arrowLocation) {
case TOP:
shape = new Area(new RoundRectangle2D.Double(insets.left, insets.top + ARROW_HEIGHT, bounds.getWidth(), bounds.getHeight() - ARROW_HEIGHT, CORNER_RADIUS, CORNER_RADIUS));
break;
case BOTTOM:
shape = new Area(new RoundRectangle2D.Double(insets.left, insets.top, bounds.getWidth(), bounds.getHeight() - ARROW_HEIGHT, CORNER_RADIUS, CORNER_RADIUS));
break;
}
// Calculate the offset for the arrow.
int centerLocationX = (int)(this.getLocation().getX() + (insets.left + bounds.getWidth()) / 2);
int offsetX = (int)(this.anchorPoint.getX() - centerLocationX);
// Create the arrow path.
GeneralPath path = new GeneralPath(GeneralPath.WIND_EVEN_ODD);
Point arrowLocation = null;
switch (this.arrowLocation) {
case TOP:
arrowLocation = new Point((int)((insets.left + bounds.width) / 2 + offsetX), insets.top + ARROW_HEIGHT);
path.moveTo(arrowLocation.getX() - ARROW_WIDTH / 2, arrowLocation.getY());
path.lineTo(arrowLocation.getX(), arrowLocation.getY() - ARROW_HEIGHT);
path.lineTo(arrowLocation.getX() + ARROW_WIDTH / 2, arrowLocation.getY());
break;
case BOTTOM:
arrowLocation = new Point((int)((insets.left + bounds.width) / 2 + offsetX), (int)(insets.top + bounds.height - ARROW_HEIGHT));
path.moveTo(arrowLocation.getX() - ARROW_WIDTH / 2, arrowLocation.getY());
path.lineTo(arrowLocation.getX(), arrowLocation.getY() + ARROW_HEIGHT);
path.lineTo(arrowLocation.getX() + ARROW_WIDTH / 2, arrowLocation.getY());
break;
}
path.closePath();
shape.add(new Area(path));
return shape;
}
/**
* Prepares a popover for presentation by calculation its bounds and the proper
* arrow location.
*/
private void prepareForPresentation() {
Dimension containerSize = PopoverView.containerView.getSize();
Point center = new Point((int)(containerSize.getWidth() / 2), (int)(containerSize.getHeight() / 2));
double offsetY = center.getY() - this.anchorPoint.getY();
// Figure out arrow location.
if (offsetY < 0.0) {
this.arrowLocation = ArrowLocation.BOTTOM;
} else {
this.arrowLocation = ArrowLocation.TOP;
}
this.updateBorder();
this.updateBounds();
this.repaint();
}
/**
* We use a border to account for the arrow.
*/
private void updateBorder() {
Border border;
switch (this.arrowLocation) {
case TOP: border = BorderFactory.createEmptyBorder(ARROW_HEIGHT, 0, 0, 0); break;
case BOTTOM: border = BorderFactory.createEmptyBorder(0, 0, ARROW_HEIGHT, 0); break;
default: border = BorderFactory.createEmptyBorder(0, 0, 0, 0); break;
}
this.setBorder(border);
}
/**
* Updates the bounds of the popover.
*/
private void updateBounds() {
Dimension size = this.getPreferredSize();
// First, try to align the popover horizontally centered to the anchor.
Point location;
switch (this.arrowLocation) {
case TOP:
location = new Point((int)(this.anchorPoint.getX() - size.getWidth() / 2), (int)this.anchorPoint.getY());
break;
case BOTTOM:
location = new Point((int)(this.anchorPoint.getX() - size.getWidth() / 2), (int)(this.anchorPoint.getY() - size.getHeight()));
break;
default:
location = new Point(0, 0);
break;
}
Rectangle bounds = new Rectangle(location, size);
// Check if the bounds fit into the container.
Rectangle containerBounds = PopoverView.containerView.getBounds();
if (!containerBounds.contains(bounds)) {
if (bounds.getX() < MINIMUM_CONTAINER_PADDING) {
bounds.setLocation(MINIMUM_CONTAINER_PADDING, (int)bounds.getY());
} else if (bounds.getX() + bounds.getWidth() > containerBounds.getWidth() - MINIMUM_CONTAINER_PADDING) {
bounds.setLocation((int)(containerBounds.getWidth() - MINIMUM_CONTAINER_PADDING - bounds.getWidth()), (int)bounds.getY());
}
}
this.setBounds(bounds);
}
/**
* Sets the border color.
*
* @param color The border color
*/
public void setBorderColor(Color color) {
this.borderColor = color;
this.repaint();
}
/**
* Gets the border color.
*
* @return The border color
*/
public Color getBorderColor() {
return this.borderColor;
}
}