package edu.mit.blocks.renderable;
import java.awt.Color;
import java.awt.Container;
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.RenderingHints;
import java.awt.Shape;
import java.awt.Toolkit;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.GeneralPath;
import java.awt.geom.RoundRectangle2D;
import javax.swing.JPanel;
import javax.swing.JTextArea;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;
import javax.swing.undo.UndoManager;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import edu.mit.blocks.codeblocks.JComponentDragHandler;
import edu.mit.blocks.codeblockutil.CScrollPane.ScrollPolicy;
import edu.mit.blocks.codeblockutil.CTracklessScrollPane;
import edu.mit.blocks.workspace.Workspace;
import edu.mit.blocks.workspace.WorkspaceEvent;
/**
* Comment stores and displays user-generated text that
* can be edited by the user. Comments begin in 'editable' state.
*
* Comments are associated with a parent source of type JComponent.
* It should "tag" along with that component. Note, however, that
* this feature should be ensured by the parent source. The
* parent source can guarantee this by invoking the methods
* setPosition, translatePosition, and setParent when
* appropriate.
*
* text : String //the text stored in this Comment and edited by the user
*/
public class Comment extends JPanel {
private static final long serialVersionUID = 328149080425L;
/**Background color of all comments*/
private static final Color background = new Color(255, 255, 150);
/**border color*/
private final Color borderColor;
/**Text field UI*/
private JTextArea textArea;//textArea belonging to editingPane
/**ScrollPane UI*/
private CTracklessScrollPane scrollPane;
/**Dragging handler of this Comment*/
private JComponentDragHandler jCompDH;
/**Manager for arrow drawn from this to parent while in editing mode**/
private CommentArrow arrow;
/**Manages Undo-able Events in this comment's text editor*/
private UndoManager undoManager;
/** The JComponent this comment and comment label is connected to */
private CommentSource commentSource;
/** The commentLabel linked to this Comment and placed on the commentSource */
private CommentLabel commentLabel;
/** true if this comment should not be able to have a location outside of its parent's bounds, false if it may be located outside of its parent's bounds */
private boolean constrainComment = true;
static int FONT_SIZE = 14;
static int MINIMUM_WIDTH = FONT_SIZE * 4;
static int MINIMUM_HEIGHT = FONT_SIZE * 2;
static int DEFAULT_WIDTH = 150;
static int DEFAULT_HEIGHT = 100;
private boolean resizing = false;
private int margin = 6;
private int width = DEFAULT_WIDTH;
private int height = DEFAULT_HEIGHT;
private double zoom = 1.0;
private String fontname = "Monospaced";
private Shape body, resize, textarea;
private boolean pressed = false;
private boolean active = false;
private final Workspace workspace;
/**
* Constructs a Comment
* with belonging to source, with text of initText, and initial zoom
* The comment's borders will have the color borderColor.
*
* Note that initializing a comment only constructs
* all of the necessary structures. To graphically display a comment,
* the implementor must then add the comment using the proper
* Swing methods OR through the convenience method Comment.setParent()
*
* @param workspace The workspace in use
* @param initText initial text of comment
* @param source where the comment is linked to.
* @param borderColor the color that the border of the comment should be
* @param zoom initial zoom
*/
public Comment(Workspace workspace, String initText, CommentSource source, Color borderColor, double zoom) {
this.workspace = workspace;
//set up important fields
this.zoom = zoom;
this.setLayout(null);
this.setOpaque(false);
this.setBounds(0, 0, DEFAULT_WIDTH, DEFAULT_HEIGHT);
this.borderColor = borderColor;
this.commentSource = source;
//set up editingPanel, labelPanel and their listeners
//initialize textArea with autowrap AROUND WORDS not characters
textArea = new JTextArea(initText);
textArea.setFont(new Font(fontname, Font.PLAIN, (int) (FONT_SIZE * zoom)));
textArea.setForeground(Color.BLACK);
textArea.setBackground(background);
textArea.setCaretColor(Color.BLACK);
textArea.setLineWrap(true);
textArea.setWrapStyleWord(true);
undoManager = new UndoManager();
undoManager.setLimit(1000);
textArea.getDocument().addUndoableEditListener(undoManager);
textArea.addKeyListener(new KeyAdapter() {
public void keyPressed(KeyEvent e) {
Comment.this.workspace.notifyListeners(new WorkspaceEvent(Comment.this.workspace, getCommentSource().getParentWidget(), WorkspaceEvent.BLOCK_COMMENT_CHANGED));
if (e.isControlDown() || ((e.getModifiers() & Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()) != 0)) {
if (e.getKeyCode() == KeyEvent.VK_Z) {
try {
undoManager.undo();
} catch (CannotUndoException exception) {
}
} else if (e.getKeyCode() == KeyEvent.VK_Y) {
try {
undoManager.redo();
} catch (CannotRedoException exception) {
}
}
}
}
});
//initialize scrollPane
scrollPane = new CTracklessScrollPane(textArea,
ScrollPolicy.VERTICAL_BAR_AS_NEEDED,
ScrollPolicy.HORIZONTAL_BAR_NEVER,
10, this.borderColor, Comment.background);
this.add(scrollPane, 0);
//set up listeners
CommentEventListener eventListener = new CommentEventListener();
this.jCompDH = new JComponentDragHandler(workspace, this);
this.addMouseListener(eventListener);
this.addMouseMotionListener(eventListener);
textArea.addMouseListener(new MouseAdapter() {
/**
* Implement MouseListener interface
*/
public void mouseEntered(MouseEvent e) {
Comment comment = Comment.this;
comment.setPressed(true);
comment.showOnTop();
}
});
textArea.addFocusListener(eventListener);
textArea.setEditable(true);
this.reformComment();
this.arrow = new CommentArrow(this);
commentLabel = new CommentLabel(workspace, source.getBlockID());
source.add(commentLabel);
commentLabel.setActive(true);
this.reformComment();
workspace.notifyListeners(new WorkspaceEvent(workspace, getCommentSource().getParentWidget(), WorkspaceEvent.BLOCK_COMMENT_ADDED));
}
/**
* Handle the removal of this comment from its comment source
*/
public void delete() {
workspace.notifyListeners(new WorkspaceEvent(workspace, getCommentSource().getParentWidget(), WorkspaceEvent.BLOCK_COMMENT_REMOVED));
getParent().remove(arrow.arrow);
setParent(null);
if (commentSource instanceof RenderableBlock) {
RenderableBlock rb = (RenderableBlock) commentSource;
rb.remove(commentLabel);
commentLabel = null;
}
}
/**
* returns the CommentSource for this comment
* @return
*/
CommentSource getCommentSource() {
return commentSource;
}
/**
* returns the commentLabel for this comment
* @return
*/
CommentLabel getCommentLabel() {
return commentLabel;
}
/**
* Returns the width of the comment label for this comment
* @return
*/
public int getCommentLabelWidth() {
if (commentLabel == null) {
return 0;
}
return commentLabel.getWidth();
}
/**
* Updates the comment and commentLabel
*/
public void update() {
if (commentLabel != null) {
setVisible(commentLabel.isActive());
commentLabel.update();
if (arrow.arrow != null) {
arrow.setVisible(commentLabel.isActive());
}
}
}
/**
* Sets the active state of the commentLabel and updates the comment and commentLabel
* @param visibleState
*/
public void update(boolean visibleState) {
if (commentLabel != null) {
commentLabel.setActive(visibleState);
}
update();
}
/**
* Set a new zoom level, changes font size, label size, location, shape of comment, and arrow for this comment
* @param newZoom
*/
public void setZoomLevel(double newZoom) {
// calculates the new position based on the initial position when zoom is at 1.0
this.zoom = newZoom;
this.textArea.setFont(new Font(fontname, Font.PLAIN, (int) (12 * zoom)));
if (commentLabel != null) {
commentLabel.setZoomLevel(newZoom);
}
this.reformComment();
this.getArrow().updateArrow();
}
/**
* Recalculate the shape of this comment
*/
public void reformComment() {
int w = textArea.isEditable() ? (int) (this.width * zoom) : (int) (Comment.MINIMUM_WIDTH * zoom);
int h = textArea.isEditable() ? (int) (this.height * zoom) : (int) (Comment.MINIMUM_HEIGHT * zoom);
int m = (int) (this.margin * zoom);
GeneralPath path2 = new GeneralPath();
path2.moveTo(m - 1, m - 1);
path2.lineTo(w - m, m - 1);
path2.lineTo(w - m, h - m);
path2.lineTo(m - 1, h - m);
path2.closePath();
textarea = path2;
body = new RoundRectangle2D.Double(0, 0, w - 1, h - 1, 3 * m, 3 * m);
GeneralPath path3 = new GeneralPath();
path3.moveTo(w - 3 * m, h);
path3.lineTo(w, h - 3 * m);
path3.curveTo(w, h, w, h, w - 3 * m, h);
resize = path3;
scrollPane.setBounds(m, m, w - 2 * m, h - 2 * m);
scrollPane.setThumbWidth(textArea.isEditable() ? 2 * m : 0);
this.setBounds(this.getX(), this.getY(), w, h);
this.revalidate();
this.repaint();
if (arrow != null) {
arrow.updateArrow();
}
}
/**
* returns the descaled x based on the current zoom
* that is given a scaled x it returns what that position would be when zoom == 1
* @param x
* @return
*/
private int descale(double x) {
return (int) (x / zoom);
}
/**
* Returns the node for this comment.
* @return
*/
public Node getSaveNode(Document document) {
Element commentElement = document.createElement("Comment");
// Text
Element textElement = document.createElement("Text");
Text text = document.createTextNode(this.getText().replaceAll("`", "'"));
textElement.appendChild(text);
commentElement.appendChild(textElement);
// Location
Element locationElement = document.createElement("Location");
Element xElement = document.createElement("X");
xElement.appendChild(document.createTextNode(String.valueOf(descale(getLocation().getX()))));
locationElement.appendChild(xElement);
Element yElement = document.createElement("Y");
yElement.appendChild(document.createTextNode(String.valueOf(descale(getLocation().getY()))));
locationElement.appendChild(yElement);
commentElement.appendChild(locationElement);
// Box size
Element boxSizeElement = document.createElement("BoxSize");
Element widthElement = document.createElement("Width");
widthElement.appendChild(document.createTextNode(String.valueOf(descale(getWidth()))));
boxSizeElement.appendChild(widthElement);
Element heightElement = document.createElement("Height");
heightElement.appendChild(document.createTextNode(String.valueOf(descale(getHeight()))));
boxSizeElement.appendChild(heightElement);
commentElement.appendChild(boxSizeElement);
// Collapse
if (!commentLabel.isActive()) {
Element collapsedElement = document.createElement("Collapsed");
commentElement.appendChild(collapsedElement);
}
return commentElement;
}
/**
* Loads the comment from a NodeList of comment parts
* @param workspace The workspace in use
* @param commentChildren
* @param rb
* @return
*/
public static Comment loadComment(Workspace workspace, NodeList commentChildren, RenderableBlock rb) {
Comment comment = null;
boolean commentCollapsed = false;
Node commentChild;
String text = null;
Point commentLoc = new Point(0, 0);
Dimension boxSize = new Dimension(Comment.DEFAULT_WIDTH, Comment.DEFAULT_HEIGHT);
for (int j = 0; j < commentChildren.getLength(); j++) {
commentChild = commentChildren.item(j);
if (commentChild.getNodeName().equals("Text")) {
text = commentChild.getTextContent();
} else if (commentChild.getNodeName().equals("Location")) {
RenderableBlock.extractLocationInfo(commentChild, commentLoc);
} else if (commentChild.getNodeName().equals("BoxSize")) {
RenderableBlock.extractBoxSizeInfo(commentChild, boxSize);
} else if (commentChild.getNodeName().equals("Collapsed")) {
commentCollapsed = true;
} else {
System.out.println("Uknown Comment Node: " + commentChild.getNodeName());
}
}
if (text != null) {
comment = new Comment(workspace, text, rb, rb.getBlock().getColor(), rb.getZoom());
comment.setLocation(commentLoc.x, commentLoc.y);
comment.update(!commentCollapsed);
comment.setMyWidth((int) boxSize.getWidth());
comment.setMyHeight((int) boxSize.getHeight());
comment.reformComment();
}
return comment;
}
/**
* overrides javax.Swing.JPanel.paint()
*/
public void paint(Graphics g) {
Graphics2D g2 = (Graphics2D) g;
g2.addRenderingHints(new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON));
if (active) {
g2.setColor(getBorderColor().brighter());
} else {
g2.setColor(getBorderColor());
}
g2.fill(body);
if (active) {
g2.setColor(Comment.background.brighter());
} else {
g2.setColor(Comment.background);
}
g2.fill(textarea);
if (active) {
g2.setColor(Color.white);
} else {
g2.setColor(Color.lightGray);
}
g2.draw(textarea);
if (active) {
g2.setColor(Color.lightGray.brighter());
} else {
g2.setColor(Color.lightGray);
}
g2.fill(resize);
super.paint(g);
}
/**
* @return this.text.trim()
*/
public String getText() {
return textArea.getText().trim();
}
/**
* @modifies editingPane, labelPane
* @effects modify eiditngPane such that the next call to
* editingPane.getText().trim() equals text.trim() &&
* modify labelPane such that the next call to
* labelPane.getText().trim() equals text.trim()
* @param text
*/
public void setText(String text) {
textArea.setText(text);
}
/**
* moves this to a new position at (x,y) but not outside of its parent Container
* @modifies this.location
* @effects Set this.location.x to x, if x is within bounds of this.parent.
* if not, then set this.location.x to closest boundary value.
* Set this.location.y to y, if y is within bounds of this.parent.
* if not, then set this.location.y to closest boundary value.
* Override javax.Swing.JComponent.setLocation()
*/
public void setLocation(int x, int y) {
if (isConstrainComment() && this.getParent() != null) {
//If x<0, set this.location.x to 0.
//If 0<x<this.parent.width, then set this.location.x to x.
//If x>this.parent.width, then set this.location.x to this.parent.width.
//repeat for y
if (y < 0) {
y = 0;
} else if (y + getHeight() > this.getParent().getHeight()) {
y = Math.max(this.getParent().getHeight() - getHeight(), 0);
}
if (x < 0) {
x = 0;
} else if (x + getWidth() + 1 > this.getParent().getWidth()) {
x = Math.max(this.getParent().getWidth() - getWidth() - 1, 0);
}
}
super.setLocation(x, y);
arrow.updateArrow();
workspace.getMiniMap().repaint();
}
/**
* moves this to a new position at (x,y) but not outside of its parent Container
* @modifies this.location
* @effects Set this.location.x to x, if x is within bounds of this.parent.
* if not, then set this.location.x to closest boundary value.
* Set this.location.y to y, if y is within bounds of this.parent.
* if not, then set this.location.y to closest boundary value.
*
* Override javax.Swing.JComponent.setLocation()
*/
public void setLocation(Point p) {
setLocation(p.x, p.y);
}
/**
* @modifies this
* @effects translate this.location
* by dx in the x-direction and dy in the y-direction
* @param dx
* @param dy
*/
public void translatePosition(int dx, int dy) {
this.setLocation(this.getX() + dx, this.getY() + dy);
}
/**
* Moves this comment from it's old parent Container to
* a new Container. Removal and addition applies only
* if the Containers are non-null
* @modifies the current this.parent and newparent
* @effects First, remove this from current this.parent ONLY if
* current this.parent is non-null. Second, add this to
* newparent container ONLY if newparent is non-null.
* Third, repaint both modified parent containers.
* @param newparent
*/
public void setParent(Container newparent) {
this.setParent(newparent, 0);
}
/**
* Over rides the standard setVisible to make sure the arrow's visibility is also set.
*/
public void setVisible(boolean b) {
super.setVisible(b);
if (arrow.arrow != null) {
arrow.setVisible(b);
}
}
/**
* Moves this comment from it's old parent Container to
* a new Container with given constrain.
* @modifies the current this.parent and newparent
* @effects First, remove this from current this.parent ONLY if
* current this.parent is non-null. Second, add this to
* newparent container ONLY if newparent is non-null.
* Third, repaint both modified parent containers.
* @param newparent
* @param constraints
*/
public void setParent(Container newparent, Object constraints) {
//though it's tempting to just write "this.setParent(newparent)"
//we can't do that because we must remove the comment as well
//remove from the current this.parent Container if non-null
Container oldParent = this.getParent();
if (oldParent != null) {
oldParent.remove(this);
oldParent.remove(arrow.arrow);
oldParent.validate();
oldParent.repaint();
}
//add this to newparent Container if non-null
if (newparent != null) {
if (constraints == null) {
newparent.add(this, 0);
} else {
newparent.add(this, constraints);
}
arrow.updateArrow();
newparent.validate();
newparent.repaint();
}
}
/**
* String representation of this
*/
public String toString() {
return "Comment ID: " + " at " + this.getLocation() + " with text: \"" + getText() + "\"";
}
/**
* Bumps the comment to top of ZOrder of parent if parent exists
*/
public void showOnTop() {
if (getParent() != null) {
getParent().setComponentZOrder(this, 0);
}
}
/**
* CommentEventListener is an inner class that
* responds to the various external events,
* and provides the requires semantic operations
* for Comments to be moved/focused correctly.
* It owns, and sends semantic actions to the
* outer Comment class.
*/
private class CommentEventListener implements FocusListener, MouseListener, MouseMotionListener {
/**When focus lost, force a repaint**/
public void focusGained(FocusEvent e) {
active = true;
repaint();
}
/**When focuses gained, force a repaint**/
public void focusLost(FocusEvent e) {
active = false;
repaint();
}
/**when clicked upon, switch to editing mode*/
public void mouseClicked(MouseEvent e) {
//prevent users from clicking multiple times and crashing the system
if (e.getClickCount() > 1) {
return;
}
}
/**highlight this comment when a mouse begins to hover over this*/
public void mouseEntered(MouseEvent e) {
showOnTop();
jCompDH.mouseEntered(e);
}
/**highlight this comment when a mouse hovers over this*/
public void mouseMoved(MouseEvent e) {
if (textArea.isEditable()) {
if (e.getX() > (width - 2 * margin) && e.getY() > (height - 2 * margin)) {
Comment.this.setCursor(new Cursor(Cursor.SE_RESIZE_CURSOR));
} else {
jCompDH.mouseMoved(e);
}
} else {
jCompDH.mouseMoved(e);
}
}
/**stop highlighting this comment when a mouse leaves this*/
public void mouseExited(MouseEvent e) {
jCompDH.mouseExited(e);
}
/**prepare for a drag when mouse is pressed down*/
public void mousePressed(MouseEvent e) {
Comment.this.grabFocus(); //atimer.stop();
showOnTop();
jCompDH.mousePressed(e);
if (textArea.isEditable()) {
if (e.getX() > (width - 2 * margin) && e.getY() > (height - 2 * margin)) {
setResizing(true);
} else {
if (e.getY() < margin) {
setPressed(true);
}
}
} else if (e.getY() < margin) {
setPressed(true);
}
repaint();
}
/**when mouse is released*/
public void mouseReleased(MouseEvent e) {
jCompDH.mouseReleased(e);
setResizing(false);
setPressed(false);
repaint();
}
/**drag this when mouse is dragged*/
public void mouseDragged(MouseEvent e) {
if (isResizing()) {
double ww = e.getX() > MINIMUM_WIDTH * zoom ? e.getX() : MINIMUM_WIDTH * zoom;
double hh = e.getY() > MINIMUM_HEIGHT * zoom ? e.getY() : MINIMUM_HEIGHT * zoom;
width = (int) ww;
height = (int) hh;
reformComment();
workspace.notifyListeners(new WorkspaceEvent(workspace, getCommentSource().getParentWidget(), WorkspaceEvent.BLOCK_COMMENT_RESIZED));
} else {
jCompDH.mouseDragged(e);
arrow.updateArrow();
workspace.notifyListeners(new WorkspaceEvent(workspace, getCommentSource().getParentWidget(), WorkspaceEvent.BLOCK_COMMENT_MOVED));
}
}
}
/**
* Returns the comment background color
* @return
*/
Color getBackgroundColor() {
return background;
}
/**
* Returns the borderColor of this comment
* @return
*/
Color getBorderColor() {
return borderColor;
}
/**
* access to the comment arrow object
* @return
*/
public CommentArrow getArrow() {
return arrow;
}
/**
* @return the width
*/
int getMyWidth() {
return width;
}
/**
* @param width the width to set
*/
void setMyWidth(int width) {
this.width = width;
}
/**
* @return the height
*/
int getMyHeight() {
return height;
}
/**
* @param height the height to set
*/
void setMyHeight(int height) {
this.height = height;
}
/**
* @return the margin
*/
int getMargin() {
return margin;
}
/**
* @param margin the margin to set
*/
void setMargin(int margin) {
this.margin = margin;
}
/**
* @return the pressed true, if this comment has been pressed
*/
boolean isPressed() {
return pressed;
}
/**
* @param pressed true if this comment has been pressed
*/
void setPressed(boolean pressed) {
this.pressed = pressed;
}
/**
* @return the resizing, true if this comment is being resized
*/
boolean isResizing() {
return resizing;
}
/**
* @param resizing true if this comment is being resized
*/
void setResizing(boolean resizing) {
this.resizing = resizing;
}
/**
* returns whether this comment should be constrained to its parent's bounds
* @return the constrainComment
*/
public boolean isConstrainComment() {
return constrainComment;
}
/**
* sets whether this comment should be constrained to its parent's bounds
* @param constrainComment the constrainComment to set
*/
public void setConstrainComment(boolean constrainComment) {
this.constrainComment = constrainComment;
}
}