package edu.mit.blocks.codeblockutil;
import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Rectangle2D;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JPanel;
import edu.mit.blocks.codeblockutil.CScrollPane.ScrollPolicy;
/**
* A StackCard is used by explorers to interface between
* a CSwing Canvas and a CSwing explorer.
*
* An explorer explores Canvases. This is the foundation
* onto which the factory UI is built. However, an
* Explorer and a Canvas are two very different objects.
* One is a high-level CSwing UI that manages the display
* of internal components and controls the position and visibility
* of internal components. The other (that is, the Canvas) is a
* low-level CSwing component that has no control over itself
* (or at least it shouldn't). An mediator is required
* to link the two objects together.
*
* We solve this interface problem by employing a
* mediator between an Explorer and a Canvas. That's
* where a StackCard comes in. A StackCard (whose visibility should be
* limited within the package) takes commands from
* it's parent Explorer and directs it's Canvas to
* follow the commands. In the opposite direction, a drawer also takes
* in user-generated actions and informs the parent
* explorer of what had just happed, so that the parent
* explorer can decide what to do with it.
*
*/
class StackCard implements PropertyChangeListener {
private int buttonHeight = 30;
/** Canvas that this StackCard renders */
private Canvas canvas;
/** A scroll pane to display as much of the Canvas as possible */
private CardPane drawerPane;
/** Parent explorer */
private StackExplorer explorer;
/** number of partitions */
private final int partitions = 5;
// (destination - origin) / partitions
/** changes in dimensions as time progresses */
private int dx, dy, dw, dh, count;
/** source and destination bounds */
private Rectangle origin, destination;
/** true iff directed to destination */
private boolean directedToDestination;
/**
* Constructs new StackCard with a parent Explorer
* @param canvas
*
* @requires canvas!=null && canvas.name!=null
* && canvas.JCOmponent != null
*/
StackCard(Canvas canvas) {
this(canvas, null);
}
/**
* Constucts a new StackCard with an explorer
* @param canvas
* @param explorer
*
* @requires canvas!=null && canvas.name!=null
* && canvas.JCOmponent != null
* @throws RuntimeException if canvas==null || canvas.name==null
* || canvas.JCOmponent == null
*/
StackCard(Canvas canvas, StackExplorer explorer) {
if (canvas == null
|| canvas.getName() == null
|| canvas.getJComponent() == null) {
throw new RuntimeException("Parameters may not be null");
}
this.canvas = canvas;
this.explorer = explorer;
this.drawerPane = new CardPane();
if (drawerPane == null) {
throw new RuntimeException("May not pass in a null instance of drawerPane");
}
this.directedToDestination = false;
count = 0;
origin = drawerPane.getBounds();
destination = drawerPane.getBounds();
dx = 0;
dy = 0;
dw = 0;
dh = 0;
canvas.getJComponent().addPropertyChangeListener(this);
}
public void propertyChange(PropertyChangeEvent e) {
if (e.getPropertyName().equals(Canvas.LABEL_CHANGE)) {
drawerPane.repaint();
}
}
/**
* notifies parent explorer that the user has selected this drawer.
*/
private void notifySelection() {
if (this.explorer != null) {
this.explorer.notifySelection(this, !directedToDestination);
}
}
/**
* True iff directed to DESTINATION
*/
boolean isDirectedToDestination() {
return this.directedToDestination;
}
/**
* sets the bounds of this drawer
* @param r
* @requires r != null
*/
void setBounds(Rectangle r) {
this.drawerPane.setBounds(r);
}
/**
* sets the bounds of this drawer
* @param x
* @param y
* @param width
* @param height
*/
void setBounds(int x, int y, int width, int height) {
this.drawerPane.setBounds(x, y, width, height);
}
Rectangle getDestination() {
return this.destination;
}
Rectangle getOrigin() {
return this.origin;
}
/**
* @return the JComponent representation of this
*/
JComponent getJComponent() {
return this.drawerPane;
}
/**
* reassigns the source bounds and the destination bounds
* of this this StackCard. When goToOrigin is called, this drawer
* moves to "origin". When gotoDestination is called,
* this Drawer moves to "destination"
* @param origin
* @param destination
*
* @requires origin != null && destination != null
*/
void reformDrawer(Rectangle origin, Rectangle destination) {
this.origin = origin;
this.destination = destination;
dx = (destination.x - origin.x) / (partitions);
dy = (destination.y - origin.y) / (partitions);
dw = (destination.width - origin.width) / (partitions);
dh = (destination.height - origin.height) / (partitions);
}
/**
* move to Origin
*/
void goToOrigin() {
this.directedToDestination = false;
}
/**
* move to destination
*/
void goToDestination() {
this.directedToDestination = true;
}
void animate() {
if (count < 0 || count > partitions) {
System.err.println("StackCard may not grow or move beyond the"
+ "boundaries set by the origin and destination bounds");
} else {
int x, y, w, h;
x = this.origin.x + dx * count;
y = this.origin.y + dy * count;
w = this.origin.width + dw * count;
h = this.origin.height + dh * count;
drawerPane.setBounds(x, y, w, h);
drawerPane.revalidate();
if (directedToDestination) {
if (count == partitions) {
//do nothing, it has already reached it's end
} else {
count++;
}
} else {
if (count == 0) {
//do nothing, it has already reached it's end
} else {
count--;
}
}
}
}
/**
* The scroll pane that displays the canvas. We must use a
* scroll pane because the size of the Canvas may be
* much greater than the size of the Drawer itself.
* @author An Ho
*/
private class CardPane extends JPanel {
private static final long serialVersionUID = 49283583495L;
/** The scroll pane that wraps the entire canvas. */
private CScrollPane scroll;
/** The drawer's label */
private CardLabel label;
/** Constructor */
private CardPane() {
super(new BorderLayout());
this.setOpaque(false);
this.scroll = new CHoverScrollPane(
canvas.getJComponent(),
ScrollPolicy.VERTICAL_BAR_AS_NEEDED,
ScrollPolicy.HORIZONTAL_BAR_AS_NEEDED,
18, canvas.getColor(), new Color(100, 100, 100, 100));
this.label = new CardLabel();
this.add(label, BorderLayout.NORTH);
this.add(scroll, BorderLayout.CENTER);
}
}
/**
* The Tabs that displays the name and hints at the colors
* of this canvas.
* @author An Ho
*/
private class CardLabel extends JButton implements ActionListener {
private static final long serialVersionUID = 3489589234L;
//To get the shadow effect the text must be displayed multiple times at
//multiple locations. x represents the center, white label.
// o is color values (0,0,0,0.5f) and b is black.
// o o
// o x b o
// o b o
// o
//offsetArrays representing the translation movement needed to get from
// the center location to a specific offset location given in {{x,y},{x,y}....}
//..........................................grey points.............................................black points
private final int[][] shadowPositionArray = {{0, -1}, {1, -1}, {-1, 0}, {2, 0}, {-1, 1}, {1, 1}, {0, 2}, {1, 0}, {0, 1}};
private final float[] shadowColorArray = {0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0, 0};
private double offsetSize = 1;
/**
* Constructs a new DrawerLabel with a shadaow-outlined text.
*/
private CardLabel() {
super();
this.setOpaque(false);
this.setBorder(null);
this.setFont(new Font("Ariel", Font.BOLD, buttonHeight - 3));
this.setPreferredSize(new Dimension(0, buttonHeight));
this.setCursor(new Cursor(Cursor.HAND_CURSOR));
this.addActionListener(this);
}
/**
* Notifies parent explorer that this drawer was selected
*/
public void actionPerformed(ActionEvent e) {
StackCard.this.notifySelection();
}
/**
* Paints this label with a shadow-outlined text and background
* matching that of the canvas's color, or grey by default.
*/
public void paint(Graphics g) {
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
//draw tab
Color highlight = canvas.getHighlight();
if (highlight == null) {
//g2.setPaint(new GradientPaint(0,0,new Color(canvas.getColor().getRed(), canvas.getColor().getGreen(), canvas.getColor().getBlue(), 100),0,this.getHeight()/3,canvas.getColor(), false));
g2.setColor(canvas.getColor());
} else {
g2.setColor(highlight);
//g2.setColor(canvas.getColor());
}
g2.fillRoundRect(0, 0, this.getWidth() - 2, 2 * this.getHeight(), this.getHeight(), this.getHeight());
if (highlight == null) {
g2.setStroke(new BasicStroke(2));
g2.setColor(new Color(255, 255, 255, 100));
} else {
g2.setStroke(new BasicStroke(2));
//g2.setColor(new Color(canvas.getColor().getRed(), canvas.getColor().getGreen(), canvas.getColor().getBlue(), 100));
//g2.setColor(new Color(240,240,40, 240));
g2.setColor(new Color(0, 0, 0, 100));
}
g2.drawRoundRect(0, 0, this.getWidth() - 2, 2 * this.getHeight(), this.getHeight(), this.getHeight());
//draw text
String text = canvas.getName();
if (text != null) {
Font font = g2.getFont().deriveFont((float) (((float) this.getHeight()) * .4));
g2.setFont(font);
FontMetrics metrics = g2.getFontMetrics();
Rectangle2D textBounds = metrics.getStringBounds(text, g2);
float x = (float) ((this.getWidth() / 2) - (textBounds.getWidth() / 2));
float y = (float) ((this.getHeight() / 2) + (textBounds.getHeight() / 2)) - metrics.getDescent();
g.setColor(Color.black);
for (int i = 0; i < shadowPositionArray.length; i++) {
int dx = shadowPositionArray[i][0];
int dy = shadowPositionArray[i][1];
g2.setColor(new Color(0, 0, 0, shadowColorArray[i]));
g2.drawString(text, x + (int) ((4 + dx) * offsetSize), y + (int) ((dy - 6) * offsetSize));
}
if (highlight == null) {
g2.setColor(Color.white);
} else {
g2.setColor(highlight);
}
g2.drawString(text, x + (int) ((4) * offsetSize), y + (int) ((-6) * offsetSize));
}
}
}
}