/*
* @(#)HTMLTextAreaFigure.java
*
* Project: JHotdraw - a GUI framework for technical drawings
* http://www.jhotdraw.org
* http://jhotdraw.sourceforge.net
* Copyright: © by the original author(s) and all contributors
* License: Lesser GNU Public License (LGPL)
* http://www.opensource.org/licenses/lgpl-license.html
*/
package CH.ifa.draw.contrib.html;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.FlatteningPathIterator;
import java.awt.image.BufferedImage;
import java.io.IOException;
import javax.swing.*;
import CH.ifa.draw.contrib.TextAreaFigure;
import CH.ifa.draw.figures.RectangleFigure;
import CH.ifa.draw.framework.*;
import CH.ifa.draw.util.Geom;
import CH.ifa.draw.util.StorableInput;
import CH.ifa.draw.util.StorableOutput;
/**
* An HTMLTextAreaFigure contains HTML formatted text.
* Formatting is done internally by a JLabel component, so all display features
* and constrains that apply for a JLabel apply also for an HTMLTextAreaFigure,
* including text and images, as in any Web browser, even allowing for contents
* residing on external Web sources. But don't forget that this is NOT a Web
* browser, so HTML contents cannot be standard Web pages with headers, stylesheets,
* javascript and who knows what else, just plain down to earth HTML code.
*
* In order to automatically integrate "external" attributes like FillColor,
* FontName, etc, HTMLTextAreaFigure encapsulates the text the user types in the
* editor within a table with a single cell occupying the whole area.
* Here is what the HTML code passed to the JLabel looks like:
*
* <html>
* <table border='0' width='area.width' height='area.height'
* cellpadding='0' cellspacing='0' bgcolor='&FillColor;'>
* <tr>
* <td width='100%'>
* <font face='&FontName;' color='&TextColor;' size='&FontSize;'>
* <!-- add italic and bold attributes if required-->
* <i>
* <b>
* ============>> User's contents go here <============
* <!-- close italic and bold attributes if required -->
* </b>
* </i>
* </font>
* </td>
* </tr>
* </table>
* </html>
*
* It is possible to write raw HTML code by calling
* setRawHTML(true)
. In that case no tags are issued.
* The user is then responsible for applying the figure attributes and in
* general is responsible for the whole display.
* This setting can be dynamically toggled as needed.
* Note that JLabel resets the font to its own default whenever it encounters
* an HTML structure, like a table or a header tag. I couldn't find a workaround
* for what can/should be called a bug. Normal browsers do not behave like this.
*
* Internal attributes like FillColor or FontName are exposed as special SGML
* entities using the standard SGML entity notation, ex: &FillColor;
.
* Any attribute associated to the figure can be used and will be replaced with
* an appropriate value from a ContentsProducer (see below) or its
* toString() value if no specific ContentProducer is defined.
*
* The HTML display and layouting can be time consuming, quite fast in most cases,
* unless the HTML structure is complicated. This can become a serious penalty
* when working with a large number of complicated figures.
* To help in this issue HTMLTextAreaFigure offers two display modes, DirectDraw,
* where the HTML layout logic is executed every time the figure is displayed, and
* BufferedDraw, where HTMLTextAreaFigure creates an in-memory image of the
* resulting layout and uses the image for fast display until a change requires
* to regenerate the image.
* The BufferedDraw mode is as fast as an image display can be, but it consumes
* more memory than the DirectDraw mode, which in turn is slower.
* The setting is specific to each figure instance and it can be dynamically
* toggled at any time, so it is possible to fine tune when and which figures
* use either one of the drawing modes.
*
* Remember the attributes based SGML entities?
* If you set the figure to be read only, so not allowing the user to directly
* edit the HTML contens, then it is possible to use HTMLTextAreaFigures to
* produce very elaborate and complex information layout.
* You create HTML templates for each figure layout you want to use and set them
* as the text of the figure. Within the template text you place field names
* wherever needed as you would for a Web page, then each figure using the template
* associates the field values as attributes of the figure. The attribute exposure
* feature will substitute the entity names with the current attribute's value.
* Please refer to the accompanying sample program to see in detail the multiple
* ways this feature can enhance your drawings.
*
* ContentProducers
* As stated above, entities referenced in the HTML template code are replaced by
* their current value in the drawn figure. The values themselves are provided
* by ContentProducers.
* For a detailed description of ContentProducers please refer to their
* own documentation, but to make it simple, a ContentProducer is an object that
* implements the method getContent
and is registered to produce
* content for either specific entities, or entity classes.
* An entity class is the class of the attribute containing its value, ie: an
* attribute containing a URL has class URL (attribute.getClass()
),
* and an URLContentProducer can be associated to it so that when the layout
* needs the entity's value, the producer's getContent() method is called and the
* returned value (ex: contents from a Web page, FTP file or disk file) is used
* to replace the entity in the displayed figure.
* The ContentProducer can return either a String, in which case it is used
* as is, or another Object. In the later case HTMLTextAreaFigure will
* continue calling registered ContentProviders depending on the class of the
* returned Object until it either gets a final String, or null. If null then
* the entity is considered as unknown and left as is in the displayed text. To
* make it dissapear alltogether the producer should return an empty String.
* HTMLTextAreaFigure registers default ContentProducers:
* AttributeFigureContentProducer for the intrinsic attributes of the figure
* (height, width, font name, etc.), URLContentProducer for URL attributes,
* HTMLColorContentProducer for HTML color encoding and for embedded
* TextAreaFigure and HTMLTextAreaFigure classes. That's right, you can embed
* a TextAreaFigure or HTMLTextAreaFigure contents inside an HTMLTextAreaFigure
* recursively for as many levels as your CPU and memory will allow.
* For instance, the main figure can consists of an HTML table where each
* cell's contents come from a different HTMLTextAreaFigure.
*
* @author Eduardo Francos - InContext
* @created 7 May 2002
* @version <$CURRENT_VERSION$>
*/
public class HTMLTextAreaFigure extends TextAreaFigure
implements HTMLContentProducerContext, FigureChangeListener {
/** Start marker for embedded attribute values */
public final static char START_ENTITY_CHAR = '&';
/** End marker for embedded attribute values */
public final static char END_ENTITY_CHAR = ';';
/** Marker escape character */
public final static char ESCAPE_CHAR = '\\';
/** holder for the image used for the display */
private transient DisposableResourceHolder fImageHolder;
/** The label used for in-memory display */
private transient JLabel fDisplayDelegate;
/** True if using direct drawing, false if using the memory image */
private boolean fUseDirectDraw = false;
/** True if the memory image should be regenerated */
private boolean fIsImageDirty = true;
/** Description of the Field */
private boolean fRawHTML = false;
/** Supplier for intrinsic data */
private transient ContentProducer fIntrinsicContentProducer;
/** Description of the Field */
private static ContentProducerRegistry fDefaultContentProducers = new ContentProducerRegistry();
// initialize the default content producers for HTMLTextAreaFigure figures
static {
fDefaultContentProducers.registerContentProducer(TextAreaFigure.class, new TextHolderContentProducer());
fDefaultContentProducers.registerContentProducer(Color.class, new HTMLColorContentProducer());
}
/** Description of the Field */
private transient ContentProducerRegistry fContentProducers = null;
/** The figure used to draw the frame of the area */
private Figure fFrameFigure = null;
// make sure required default attributes are set
static {
initDefaultAttribute("XAlignment", new Integer(SwingConstants.LEFT));
initDefaultAttribute("YAlignment", new Integer(SwingConstants.TOP));
initDefaultAttribute("LeftMargin", new Float(5));
initDefaultAttribute("RightMargin", new Float(5));
initDefaultAttribute("TopMargin", new Float(5));
initDefaultAttribute("BottomMargin", new Float(5));
initDefaultAttribute("TabSize", new Float(8));
}
/** Constructor for the HTMLTextAreaFigure object */
public HTMLTextAreaFigure() {
initialize();
}
/**
* Clones a figure and initializes it
*
* @return Description of the Return Value
* @see Figure#clone
*/
public Object clone() {
Object cloneObject = super.clone();
((HTMLTextAreaFigure)cloneObject).initialize();
return cloneObject;
}
/**
* Sets the display box for the figure
*
* @param origin origin point
* @param corner corner point
* @see Figure
*/
public void basicDisplayBox(Point origin, Point corner) {
super.basicDisplayBox(origin, corner);
getFrameFigure().displayBox(displayBox());
}
/**
* Returns an iterator of standard sizing handles to manipulate the figure
*
* @return Description of the Return Value
*/
public HandleEnumeration handles() {
return getFrameFigure().handles();
// List handles = CollectionsFactory.current().createList();
// BoxHandleKit.addHandles(this, handles);
// return new HandleEnumerator(handles);
}
/**
* True if the figure contains the point. The call is relayed to the frame figure
*
* @param x Description of the Parameter
* @param y Description of the Parameter
* @return Description of the Return Value
*/
public boolean containsPoint(int x, int y) {
return getFrameFigure().containsPoint(x, y);
}
/**
* Moves the figure by the specified displacement
*
* @param dx Description of the Parameter
* @param dy Description of the Parameter
*/
public void moveBy(int dx, int dy) {
super.moveBy(dx, dy);
getFrameFigure().moveBy(dx, dy);
}
/** Initializes the figure */
protected void initialize() {
fImageHolder = DisposableResourceManagerFactory.createStandardHolder(null);
setFrameFigure(new RectangleFigure());
// initialize the content producers
setIntrinsicContentProducer(new HTMLContentProducer());
fContentProducers = new ContentProducerRegistry(fDefaultContentProducers);
markSizeDirty();
markImageDirty();
markTextDirty();
markFontDirty();
setAttribute(Figure.POPUP_MENU, createPopupMenu());
}
/**
* Called whenever the something changes that requires size recomputing
*/
protected void markSizeDirty() {
markImageDirty();
super.markSizeDirty();
}
/**
* Called whenever the something changes that requires text recomputing
*/
protected void markTextDirty() {
markImageDirty();
super.markTextDirty();
}
/**
* Called whenever the something changes that requires font recomputing
*/
protected void markFontDirty() {
markImageDirty();
super.markFontDirty();
}
/**
* Draws the figure in the given graphics. Draw is a template
* method calling drawBackground followed by drawText then drawFrame.
* HTMLTextAreaFigure displays in a different order tahn most figures to avoid
* smearing of the border when enclosed in a weird frame figure.
* Also, there is no such thing as a transparent background so we always draw it.
*
* @param g Description of the Parameter
* @todo check possibility of clipping the contents from the background to have a
* transparent figure
*/
public void draw(Graphics g) {
Color fill = getFillColor();
g.setColor(fill);
drawBackground(g);
// we draw the text then the rame to avoid smearing
drawText(g, displayBox());
Color frame = getFrameColor();
g.setColor(frame);
drawFrame(g);
}
/**
* Draws the frame around the text. It gets the shape of the frame from the
* enclosing figure
*
* @param g The graphics to use for the drawing
*/
public void drawFrame(Graphics g) {
((Graphics2D)g).draw(getClippingShape());
}
/**
* Draws the background for the figure. It gets the shape of the frame from the
* enclosing figure
*
* @param g The graphics to use for the drawing
*/
public void drawBackground(Graphics g) {
((Graphics2D)g).fill(getClippingShape());
}
/**
* Formats and draws the text for the figure
*
* @param g the graphics for the drawing. It can be null when
* called just to compute the size
* @param displayBox the display box within which the text should be formatted and drawn
* @return Description of the Return Value
*/
protected float drawText(Graphics g, Rectangle displayBox) {
Graphics2D g2 = null;
Shape savedClip = null;
if (g != null) {
g2 = (Graphics2D)g;
savedClip = g2.getClip();
}
Rectangle drawingBox = makeDrawingBox(displayBox);
// drawing an empty displayBox is not possible
if (drawingBox.isEmpty()) {
return drawingBox.height;
}
if (g != null) {
g2.clip(getClippingShape());
}
if (usesDirectDraw()) {
drawTextDirect(g2, drawingBox);
}
else {
fImageHolder.lock();
if (isImageDirty()) {
generateImage(drawingBox);
setSizeDirty(false);
}
if (g2 != null) {
g2.drawImage(getImage(), drawingBox.x, drawingBox.y, null);
}
fImageHolder.unlock();
}
if (g != null) {
g2.setClip(savedClip);
}
// redraw the border to prevent smearing
drawFrame(g);
return displayBox.height;
}
/**
* Generates the HTML image to be used for fast BufferedDrawing
*
* @param drawingBox Description of the Parameter
*/
protected void generateImage(Rectangle drawingBox) {
// create the image and get its Graphics
createImage(drawingBox.width, drawingBox.height);
Graphics2D g2 = (Graphics2D)getImage().getGraphics();
Rectangle finalBox = new Rectangle(drawingBox);
finalBox.setLocation(0, 0);
renderText(g2, finalBox);
g2.dispose();
}
/**
* Draws the text directly onto the drawing, without using the cached figure
*
* @param g2 Description of the Parameter
* @param drawingBox Description of the Parameter
*/
protected void drawTextDirect(Graphics2D g2, Rectangle drawingBox) {
Shape savedClipArea = null;
Color savedFontColor = null;
//Font savedFont = null;
//Rectangle2D clipRect = null;
RenderingHints savedRenderingHints = null;
if (g2 != null) {
savedRenderingHints = g2.getRenderingHints();
savedClipArea = g2.getClip();
//savedFont = g2.getFont();
savedFontColor = g2.getColor();
g2.clip(drawingBox);
}
//float finalHeight = renderText(g2, drawingBox);
// restore saved graphic attributes
if (g2 != null) {
g2.setClip(savedClipArea);
g2.setColor(savedFontColor);
g2.setRenderingHints(savedRenderingHints);
}
}
/**
* Renders the HTML formatted text onto the supplied Graphics.
* Rendering involves entity substitution and HTML contents preparation suitable
* for display by a JLabel.
*
* @param g2 Description of the Parameter
* @param drawingBox Description of the Parameter
* @return Description of the Return Value
* @todo look for other HTML display providers as JLabel is kind of
* lousy at it
*/
protected float renderText(Graphics2D g2, Rectangle drawingBox) {
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2.setRenderingHint(RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_QUALITY);
// fill with background color
g2.setBackground(getFillColor());
g2.setColor(getFillColor());
g2.clearRect(drawingBox.x, drawingBox.y, drawingBox.width, drawingBox.height);
g2.fillRect(drawingBox.x, drawingBox.y, drawingBox.width, drawingBox.height);
// get the text. Either in raw format or prepared for HTML display
String text;
if (isRawHTML()) {
text = getText();
}
else {
text = getHTMLText(getText(), getFont(),
(String)getContentProducer(Color.class).getContent(this, FigureAttributeConstant.TEXT_COLOR_STR, getTextColor()),
(String)getContentProducer(Color.class).getContent(this, FigureAttributeConstant.FILL_COLOR_STR, getFillColor()),
drawingBox
);
}
// perform entity keyword substitution
text = substituteEntityKeywords(text);
// create the JLabel used as delegate for drawing
JLabel displayDelegate = getDisplayDelegate();
displayDelegate.setText(text);
displayDelegate.setBackground(getFillColor());
// ensure the label covers the whole area
displayDelegate.setLocation(0, 0);
displayDelegate.setSize(drawingBox.width, drawingBox.height);
displayDelegate.setHorizontalAlignment(((Integer)getAttribute("XAlignment")).intValue());
displayDelegate.setVerticalAlignment(((Integer)getAttribute("YAlignment")).intValue());
// finally display it
SwingUtilities.paintComponent(
g2,
displayDelegate,
getContainerPanel(displayDelegate, drawingBox),
drawingBox.x,
drawingBox.y,
drawingBox.width,
drawingBox.height);
return drawingBox.height;
}
/**
* Builds the drawing box using the margins
*
* @param displayBox Description of the Parameter
* @return The drawing box
*/
protected Rectangle makeDrawingBox(Rectangle displayBox) {
// get alignment information
float leftMargin = ((Float)getAttribute("LeftMargin")).floatValue();
float rightMargin = ((Float)getAttribute("RightMargin")).floatValue();
float topMargin = ((Float)getAttribute("TopMargin")).floatValue();
float bottomMargin = ((Float)getAttribute("BottomMargin")).floatValue();
// inset the drawing box by 1 on every side so as not to overwrite
// the border
Rectangle drawingBox = new Rectangle(displayBox);
drawingBox.grow(-1, -1);
// adjust for margins
drawingBox.x += leftMargin;
drawingBox.width -= (leftMargin + rightMargin);
drawingBox.y += topMargin;
drawingBox.height -= topMargin + bottomMargin;
return drawingBox;
}
/**
* Gets the displayDelegate attribute of the HTMLTextAreaFigure object
*
* @return The displayDelegate value
*/
protected JLabel getDisplayDelegate() {
if (fDisplayDelegate == null) {
fDisplayDelegate = new JLabel();
fDisplayDelegate.setBorder(null);
}
return fDisplayDelegate;
}
/**
* Creates the cached image, unless there is already one and it is
* compatible with new request, in which case we reuse it
*
* @param width Description of the Parameter
* @param height Description of the Parameter
*/
protected void createImage(int width, int height) {
// if current image is compatible reuse it
fImageHolder.lock();
if (!fImageHolder.isAvailable() ||
((BufferedImage)fImageHolder.getResource()).getWidth() != width ||
((BufferedImage)fImageHolder.getResource()).getHeight() != height) {
fImageHolder.setResource(new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB));
}
// we don't unlock the image, it's the responsibility of
// the caller to do so, this in oreder to ensure that calling createImage
// will always return with a valid image
}
/**
* Builds the container for the drawing delegate
*
* @param drawingDelegate The delegate
* @param displayBox The bounding box
* @return The container
*/
protected JPanel getContainerPanel(Component drawingDelegate, Rectangle displayBox) {
JPanel panel = new JPanel();
return panel;
}
/**
* Returns a string that is valid HTML contents for a JLabel.
* Valid HTML contents is text enclosed in tags.
* We enclose the supplied text into a table with a single cell so that we
* can also set the external alignment and font attributes
*
* @param text The text
* @param font The font
* @param textColor The text color HTML code
* @param backColor The background's color HTML code
* @param displayBox Description of the Parameter
* @return The final HTML encoded text
*/
protected String getHTMLText(String text, Font font, String textColor,
String backColor, Rectangle displayBox) {
StringBuffer htmlText = new StringBuffer();
// add an
htmlText.append("");
// add a table with width=100%, background color, and no borders with
// a single cell
htmlText.append(
"
");
// set the font
htmlText.append("");
// add alignment if required
if (((Integer)getAttribute("XAlignment")).intValue() == SwingConstants.CENTER) {
htmlText.append(" |
>font face='&FontName;' color='&FillColor;'<