Webber: A Website Construction Application


HtmlPage.java


// HTML text viewer component

package library.html;

import java.awt.*;
import java.net.*;
import java.util.*;
import java.awt.event.*;
import java.awt.image.*;

import library.*;					// Import library classes

// HTML Page Display
public class HtmlPage extends Canvas implements KeyListener {

	// General internal variables
	char[] text;					// The HTML text
	HtmlReader reader;				// HTML text reader object
	private DrawStringBuffer lineBuffer;		// Buffer for Graphics.drawString
	private boolean hidden, update, spaced;		// Internal state flags
	private Point centre;				// Point used for line-centre
	private int newLines;				// Newlines counter

	// Font settings
	private String fontName;			// Internal font name
	private int fontSize;				// Internal font size
	private boolean bold, italic, underline;	// Internal font settings
	private boolean validFont;			// Update-system-font flag

	// Font metrics
	private FontMetrics fm;				// Current metrics object
	private int ascent, descent;			// Font height
	int tabsize;					// Width of a TAB character

	// Page settings
	private int borderX = 10, borderY = 14;		// Text borders
	private int indentLeft, indentRight;		// Text indentation
	private int xpos, ypos;				// Output cursor location
	private int lineTop;				// Current top of line
	private boolean isPre;				// Pre-formatted mode flag
	private boolean isPlain;			// Plain-text format flag
	int left, top;					// Window view offsets
	int listDepth;					// Current list depth

	// Page metrics
	private Dimension htmlSize;			// Total HTML width/height
	private int linewidth, pageright, pageheight;	// Window-relative metrics
	private int vAdvance;				// Vertical advance height
	private int alignStart;				// Alignment start position
	int lineheight;					// Normal font height

	// Return the HtmlReader being used to read tag objects
	public HtmlReader getHtmlReader() { return reader; }

	// Return a fully resolved URL or String reference for the specified filename
	// Overridden by objects which provide file I/O for the HtmlPage (eg HtmlFile)
	public Object getHref(String name) { return name; }

	// Open a URL and replace the current text (overridden as above)
	public boolean openUrl(String addr) { return false; }

	// Return normal text color settings
	protected Color getNormalColor() { return Color.black; }
	protected Color getLinkColor() { return Color.blue; }

	// Invalidate the current font settings for a call to updateFont()
	private void newFont() { validFont = false; }

	// Set current font settings from Font
	public synchronized void setFont(Font f) {
		fontName	= f.getName();
		fontSize	= f.getSize();
		bold		= f.isBold();
		italic		= f.isItalic();
		newFont();
	}

	// Default font settings (override to change defaults)
	protected Font getNormalFont() { return new Font("Serif", Font.PLAIN, 16); }
	protected Font getTeleFont() { return new Font("Monospaced", Font.PLAIN, 14); }

	// Select default fonts
	protected void setNormalFont() { setFont(getNormalFont()); }
	protected void setTeleFont() { setFont(getTeleFont()); }

	// Return the current font for this state
	public Font getFont() {
		if (fontName==null) return getNormalFont();	// Initialization font
		int style = (bold?Font.BOLD:0) | (italic?Font.ITALIC:0);
		return new Font(fontName, style, fontSize);	// Active font
	}

	// Set the vertical advance height
	private void setVerticalAdvance(int h) { if (h > vAdvance) vAdvance = h; }

	// Read and set current font metrics
	private void setFontMetrics(Graphics g) {
		fm = g.getFontMetrics();			// Get metrics object
		ascent = fm.getMaxAscent();			// Get font ascent
		descent = fm.getMaxDescent();			// Get font descent
		tabsize = fm.stringWidth("_") * 8;		// Calculate tab width
		setVerticalAdvance(getLineHeight());		// Update max line height
	}

	// Set current font and update metrics (if necessary)
	private void updateFont(Graphics g) {
		if (!validFont) {				// Not already set?
			g.setFont(getFont());			// Set Graphics font
			setFontMetrics(g);			// Get font metrics
			validFont = true;			// Validate font
		}
	}

	// Change individual settings for the current font
	protected void setFontName(String name)	   { fontName = name; newFont(); }
	protected void setFontSize(int size)	   { fontSize = size; newFont(); }
	protected void setBold(boolean state)	   { bold = state;    newFont(); }
	protected void setItalic(boolean state)	   { italic = state;  newFont(); }
	protected void setUnderline(boolean state) { underline = state; }

	// Restore individual font settings to normal
	protected void setFontName() { setFontName(getNormalFont().getName()); }
	protected void setFontSize() { setFontSize(getNormalFont().getSize()); }

	// Initialize font settings
	private void initFont() { setUnderline(false); setNormalFont(); }

	// Initialize page state for drawing
	private void initPage() {
		listDepth = 0;				// Initialize list depth
		indentLeft = indentRight = 0;		// Reset indentation
		spaced = false;				// Reset spacing-flag
		newLines = 2;				// Reset newline counter
		xpos = borderX;				// Initial x-coordinate
		ypos = borderY;				// Initial y-coordinate
		lineTop = ypos;				// Store top of line
		vAdvance = 0;				// Reset maximum line height
		alignStart = borderX;			// Reset alignment start pos
	}

	// Read and set current page metrics
	private void setPageMetrics() {
		Dimension d = getSize();
		linewidth = d.width - borderX*2 - indentLeft - indentRight;
		pageright = d.width - borderX - indentRight;
		pageheight = d.height;
	}

	// Set the page to the home (top-left) position
	private void home() { left = top = 0; }

	// Returns true if the current line is visible
	private boolean isLineVisible() { return (!hidden) && ((ypos+ascent)>=top); }

	// Returns true if position x is past the right of this page
	private boolean isPageRight(int x) { return (x >= pageright); }

	// Returns true the current position is past the bottom of this page
	private boolean isPageBottom() { return (ypos >= top+pageheight); }

	// Returns true if currently at the start of an empty line
	private boolean isNewLine() { return (newLines > 0); }

	// Return the current height of a line of text
	private int getLineHeight() { return ascent + descent; }

	// Return the page starting position
	int getPageStart() { return borderY; }

	// Return the line starting position
	int getLineStart() { return borderX + indentLeft; }

	// Return the number of normal-height lines which would fit on this page
	int getNumLines() { return pageheight / lineheight; }

	// Add a height offset to the total HTML height (used by HtmlEdit)
	void addHtmlHeight(int height) { htmlSize.height += height; }

	// Set new text for this page
	private void resetText(char[] text) {
		this.text = text;				// Set new text object
		if (isPlain) reader.setPlainText(text);		// Plain text or HTML?
		else reader.setText(text);			// Build a new tag list
		update = true;					// New metrics (via paint)
	}

	// Update the text after adding offset chars at pos (partial tag-list rebuild)
	protected void updateText(char[] text, int pos, int offset) {
		this.text = text;				// Set new text object
		if (isPlain) reader.setPlainText(text);		// Plain text or HTML?
		else reader.updateText(text, pos, offset);	// Update the tag list
	}

	// Initialize page offsets and settings and update text
	public void setText(char[] text, boolean isPlain) {
		home();  this.isPlain = isPlain;  resetText(text);
	}

	// Return the HTML text
	public char[] getText() { return text; }

	// Returns true if text should be displayed in pre-formatted mode
	public boolean isPlainText() { return false; }

	// Set the text-formatting mode for this page (normal or preformatted)
	public void setPreformat(boolean state) { isPre = state; }

	// Enable/disable line-centre (enable from the current location)
	public void setCentre(boolean state) {
		centre = (state==false) ? null : new Point(borderX, ypos);
	}

	// Fill an area with the component's background colour
	private void clearArea(int x, int y, int w, int h, Graphics g) {
		Lib.fillArea(x, y, w, h, getBackground(), g);
	}

	// Offset any click areas on the line currently being centred
	private void centreClickAreas(int offset) {
		Enumeration e = clickAreas.elements();		// Get ClickArea list
		boolean moveArea = false;			// True if target line
		ClickArea a;
		while (e.hasMoreElements()) {			// End of list?
			a = (ClickArea) e.nextElement();	// Get next area
			if (!moveArea)				// At target line?
				if (a.start.y >= centre.y) moveArea = true;
			if (moveArea) {				// Move this area?
				a.start.x += offset;		// Offset start pos
				a.end.x += offset;		// Offset end pos
			}
		}
	}

	// Centre the line just drawn
	private void centreline(Graphics g) {
		if (centre==null) return;			// Centre not active?
		int w = xpos-centre.x;				// Get text width
		if (w < 1) return;				// Empty line?
		int h = ypos-centre.y, offset = (linewidth - w) / 2;
		g.copyArea(centre.x, centre.y, w, h, offset, 0);// Translate text
		clearArea(centre.x, centre.y, offset, h, g);	// Crop copied area
		centreClickAreas(offset);			// Adjust click areas
	}

	// Align text preceding an image and set alignment for following text
	private void alignText(Rectangle r, int align, Graphics g) {
		setVerticalAdvance(r.height);			// Set line height
		if (align==2) return;				// Align top
		int offset = (align==1) ? r.height / 2		// Align middle
					: r.height;		// Align bottom
		if ((offset -= ascent) < 0) return;		// Trap negative offset
		int w = xpos - alignStart, h = getLineHeight();	// Get text dimensions
		g.copyArea(alignStart, ypos, w, h, 0, offset);	// Move text
		clearArea(alignStart, ypos, w, (h < offset) ? h : offset, g);
		ypos += offset;					// Add line offset
	}

	// Attempt to start loading and drawing an image (wait for width & height)
	private boolean startImage(Image i, int x, int y, Graphics g) {
		if (g.drawImage(i, x, y, this)) return true;	// Image is ready?
		int f, size = ImageObserver.WIDTH | ImageObserver.HEIGHT,
		       quit = ImageObserver.ERROR | ImageObserver.ABORT;
		while (((f = checkImage(i, this)) & size) != size)
			if ((f & quit) != 0) return false;	// Load failed
		return true;					// Load started
	}

	// Create and return a click area for image or null if no click area is active
	private ClickArea startImageClickArea(int w, int h, Graphics g) {
		ClickArea a = getClickArea();			// Get active area
		if (a==null) return null;			// No active area?
		endClickArea();					// End text area
		addClickArea(a.listener);			// Add image area
		a = getClickArea();				// Get image area
		endClickArea(a.start.x + w, a.start.y + h, g);	// End image area
		return a;					// Return new area
	}

	// Draw an image at the current location
	private LinkList images = new LinkList();
	public void drawImage(Image i, Graphics g) { drawImage(i, 0, g); }
	public void drawImage(Image i, int align, Graphics g) {
		if (isPre) return;	// No images if pre-formatted (to be continued)
		if (ypos!=lineTop) ypos = lineTop;		// Restore top of line
		if (!startImage(i, xpos, ypos, g)) return;	// Attempt image load
		int w = i.getWidth(this), h = i.getHeight(this);// Get image size
		ClickArea a = startImageClickArea(w, h, g);	// Modify click area
		Rectangle r = new Rectangle(xpos, ypos, w, h);	// Create image locator
		images.addElement(r);				// Store image location
		alignText(r, align, g);				// Align adjacent text
		xpos += w;					// Advance position
		alignStart = xpos;				// Set align start pos
		newLines = 0;					// Reset newline count
		if (a!=null) addClickArea(a.listener);		// Restart click area
	}

	// Override Component to draw complete images only
	public boolean imageUpdate(Image img, int flags, int x, int y, int w, int h) {
		if ((flags & (FRAMEBITS|ALLBITS)) != 0)		// Complete image?
			repaint();				// Repaint component
		return (flags & (ALLBITS|ABORT)) == 0;		// False if complete
	}

	// Move xpos to skip over any images between xpos and xpos+w
	private void skipImages(int w) {
		boolean skipped = false;			// Reset skipped flag
		Enumeration e = images.elements();		// Get active images
		while (e.hasMoreElements()) {			// Check images for x
			Rectangle r = (Rectangle)e.nextElement();
			if ((xpos < r.x + r.width) && (xpos + w > r.x)) {
				xpos = r.x + r.width;		// Skip over the image
				skipped = true;			// Set skipped flag
			} else if (skipped) break;		// Clear after skipping?
		}
		if (skipped) alignStart = xpos;			// Set align start pos
	}

	// Remove all images cleared by the current line (ypos)
	private void clearImages() {
		Enumeration e = images.elements();		// Get active images
		while (e.hasMoreElements()) {			// Remove cleared images
			Rectangle r = (Rectangle)e.nextElement();
			if (ypos >= r.y + r.height) images.removeElement(r);
		}
	}

	// Centre line (if required) and move to the start of the next line
	// Also breaks and continues the area monitored by a ClickListener (if any)
	private void endline(boolean hidden, Graphics g) {
		ClickArea a = getClickArea();			// Get active click area
		if (a!=null) {					// Click area is active?
			a.isMulti = true;			// Set multi-line flag
			endClickArea();				// Terminate click area
		}
		updateFont(g);					// Validate current font
		newLines++;					// Inc. newline count
//		ypos += getLineHeight();			// Move to the next line
//		ypos += vAdvance;				// Advance past line
		ypos = lineTop + vAdvance;			// Advance past line
		lineTop = ypos;					// Store top of line
		vAdvance = getLineHeight();			// Reset vertical advance
		if (!hidden) centreline(g);			// Centre visible line?
		else if (xpos>htmlSize.width) htmlSize.width = xpos;	// Get metrics
		xpos = getLineStart();				// Move to start of line
		alignStart = borderX;				// Reset align start pos
		if (centre!=null) setCentre(true);		// Reset line-centre
		clearImages();					// Remove cleared images
		if (a!=null) addClickArea(a.listener);		// Continue click area?
	}

	// Move current position to the start of the next line
	public boolean nextline(Graphics g) {
		lineBuffer.flush();
		endline(hidden, g);
		return hidden ? true : !isPageBottom();
	}

	// Move current position to the next line only if necessary
	public boolean newline(Graphics g) {
		return (isNewLine()) ? true : nextline(g);
	}

	// Move current position to the start of the next paragraph
	public boolean newpara(Graphics g) {
		for (int i=newLines; i<2; ++i)
			if (!nextline(g)) return false;
		return true;
	}

	// Add values to the left & right indents
	private void addIndent(int left, int right) {
		lineBuffer.flush();
		indentLeft += left;
		indentRight += right;
		xpos = getLineStart();
		setPageMetrics();		// Update page metrics
	}
	public void addIndent(int indent) { addIndent(indent, 0); }
	public void addIndents(int indent) { addIndent(indent, indent); }

	// Write a word or a space and update the current position (with underlining)
	private boolean drawWord(String word, Graphics g) {
		int w = fm.stringWidth(word);			// Get word width
		skipImages(w);					// Skip over any images
		if (isPageRight(xpos+w))			// Past end of line?
			if (w<=linewidth) {			// Fit on next line?
				if (!nextline(g)) return false;	// Move to next line
				else if (word.charAt(0)==' ') return true;
			} else if (!isNewLine())		// Next line is longer?
				if (!nextline(g)) return false;	// Move to next line
		if (isLineVisible()) {				// Visible line?
			int y = ypos+ascent;			// Get text baseline
//			g.drawString(word, xpos, y);		// Non-buffer drawing
			lineBuffer.drawString(word, xpos, y, g);// Draw via buffer
			if (underline) g.drawLine(xpos, y+2, xpos+w-1, y+2);
		}
		xpos += w;					// Advance current xpos
		newLines = 0;					// Reset newline count
		return true;
	}

	// Return the character represented by a character-conversion code
	private static final String[]				// Code strings
		codeString = { "lt", "gt", "amp", "quot", "nbsp", "reg", "copy" };
	private static final char[]				// Code characters
		codeChar = { '<', '>', '&', '"', ' ', '\u00AE', '\u00A9'};
	private static char getCodeChar(String code) {
		if (code==null) return 0;			// No code string?
		if (code.length()==0) return 0;			// Empty code string?
		if (code.charAt(0)=='#')			// Character value?
			return (char)Integer.valueOf(code.substring(1)).intValue();
		for (int i=0; i < codeString.length; ++i)	// Search code array
			if (code.equals(codeString[i]))		// Compare codes
				return codeChar[i];		// Return character
		return 0;					// Code not found
	}

	// Perform character conversions before drawing text
	private static String parseText(String s) {
		StringBuffer buf = new StringBuffer(s.length());// Get a string buffer
		boolean isCoded = false;			// Reset conversion flag
		String code;
		char c, ch;
		for (int i=0, num=s.length(); i < num; ++i) {	// Check each character
			if ((ch = s.charAt(i))=='&') {		// Conversion character?
				code = Lib.getDelimitedString(s, i, '&', ';');
				if ((c = getCodeChar(code))!=0) {
					ch = c;			// Convert character
					i += code.length() + 1;	// Skip code & semi
					isCoded = true;		// Set conversion flag
				}
			}
			buf.append(ch);				// Add char to buffer
		}
		return isCoded ? buf.toString() : s;		// Converted or original
	}

	// Draw a either a word or (for whitespace characters) a space
	private boolean putText(String s, String delim, Graphics g) {
		if (delim.indexOf(s.charAt(0)) >= 0)		// Whitespace?
			if (isNewLine()) return true;		// Start of line?
			else if (spaced) return true;		// Already spaced?
			else { s = " "; spaced = true; }	// Translate to space
		else {						// Non-whitespace (text)
			spaced = false;				// Indicate no spaces
			s = parseText(s);			// Convert characters
		}
		return drawWord(s, g);				// Output on display
	}

	// Draw a line of pre-formatted text (with tabs) and return new xpos
	// Output is prevented if g is null to allow text to be measured
	int textLine(String s, int x, int y, Graphics g) {
		String delim = "\t";
		StringTokenizer st = new StringTokenizer(s, delim, true);
		int inset = getLineStart();
		y += ascent;
		while (st.hasMoreTokens()) {
			s = st.nextToken();
			if (delim.indexOf(s.charAt(0))>=0) {	// Tabs?
				for (int i=s.length(); i>0; --i)
					x += -((x-inset) % tabsize) + tabsize;
			} else {				// Text & spaces
				int w = fm.stringWidth(s);	// Get total width
				if (g!=null) {	// Display? (not just measuring)
					g.drawString(s, x, y);
					if (underline) g.drawLine(x, y+2, x+w-1, y+2);
				}
				x += w;				// Increment position
			}
		}
		return x;
	}

	// Draw (or measure if not visible) a line of pre-formatted text (with tabs)
	private void putLine(String s, Graphics g) {
		xpos = textLine(s, xpos, ypos, isLineVisible() ? g : null);
	}

	// Execute a newline for every '\n' character in s
	private boolean putNewLines(String s, Graphics g) {
		for (int i=s.length(); i>0; --i)
			if (s.charAt(i-1)=='\n')
				if (!nextline(g)) return false;
		return true;
	}

	// Helper classes used to extend the functionality of drawText
	class TextWriter {				// Writes text in HTML format
		String getDelimiters() { return "\r\n \t"; }
		StringTokenizer getTokenizer(String s) {// Get an appropriate tokenizer
			return new StringTokenizer(s, getDelimiters(), true);
		}
		boolean write(String s, Graphics g) {	// Write a word or a space
			return putText(s, getDelimiters(), g);
		}
	}
	class PreTextWriter extends TextWriter {	// Writes pre-formatted text
		String getDelimiters() { return "\r\n"; }
		boolean write(String s, Graphics g) {	// Write text line or newlines
			if (getDelimiters().indexOf(s.charAt(0)) < 0) putLine(s, g);
			else if (!putNewLines(s, g)) return false;
			return true;
		}
	}

	// Draw text on page in normal or pre-formatted mode
	public boolean drawText(String s, Graphics g) {
		TextWriter w;				// Text writer helper-object
		if (!isPre) w = new TextWriter();	// HTML-format text
		else w = new PreTextWriter();		// Pre-formatted text
		boolean done = true;			// Set end-of-page flag
		updateFont(g);				// Set font if required
		StringTokenizer st = w.getTokenizer(s);	// Get text tokenizer
		while (st.hasMoreTokens()) {
			s = st.nextToken();		// Get & draw next line/word
			if (!w.write(s, g)) { done = false; break; }
		}
		lineBuffer.flush();			// Draw remaining text
		return done;
	}

	// Draw all tag-objects on this page (visible or hidden)
	private void drawPage(boolean hidden, Graphics g) {
		initPage();				// Initialize page state
		setPageMetrics();			// Initialize page settings
		initFont();				// Initialize font settings
		if (isPlain) setPreformat(true);	// Plain text mode
		else setPreformat(isPlainText());	// Set text-formatting mode
		updateFont(g);				// Apply initial font settings
		setCentre(false);			// Initialize centre-line state
		this.hidden = hidden;			// Set visibility mode
		clickAreas.removeAllElements();		// Initialize click area list
		reader.drawText(this, g);		// Draw tags using HtmlReader
		endClickArea();				// Terminate click area (if any)
	}

	// Recalculate global page metrics (lineheight & htmlSize)
	private void updatePage(Graphics g) {
		htmlSize = new Dimension();		// Initialize HTML dimensions
		initFont();				// Initialize default font
		updateFont(g);				// Set Graphics font
		lineheight = getLineHeight();		// Get normal line height
		drawPage(true, g);			// Draw page in hidden mode
		htmlSize.height = ypos;			// Set total page height
		update = false;				// Reset update flag
	}

	// Repaint this page - recalculates lineheight and htmlSize if updated
	public void paint(Graphics g) {
		if (update) updatePage(g);		// First call since update?
		GraphicsBuffer buf = new GraphicsBuffer(); // Get a double-buffer
		Dimension d = getSize();		// Modify buffer width
		if (htmlSize.width > 0) d.width = htmlSize.width;
		Rectangle r = new Rectangle(0, 0, d.width, d.height);
		g = buf.getGraphics(this, r, g);	// Get hidden Graphics context
		g.translate(0, -top);			// Translate vertical view
		drawPage(false, g);			// Draw page in visible mode
		buf.paint(-left, 0);			// Translate image to display
		buf.crop(-left, 0);			// Crop to the image edges
//		requestFocus();				// For keyboard input (no need?)
	}

	// Override Component to skip clearing the component (due to GraphicsBuffer)
	public void update(Graphics g) { g.setColor(getForeground()); paint(g); }

	// Process keys
	public void keyPressed(KeyEvent e) {
		switch (e.getKeyCode()) {
		    case KeyEvent.VK_UP:		// Up arrow
		    	if (top<=0) return;
		    	if ((top -= lineheight)<0) top = 0;
		    	break;
		    case KeyEvent.VK_DOWN:		// Down arrow
		    	if (top+pageheight+lineheight>=htmlSize.height+borderY) return;
		    	top += lineheight;
		    	break;
		    case KeyEvent.VK_LEFT:		// Left arrow
			if (left<=0) return;
			if ((left -= tabsize)<0) left = 0;
			break;
		    case KeyEvent.VK_RIGHT:		// Right arrow
		    	left += tabsize;
		    	break;
		    case KeyEvent.VK_PAGE_UP:		// Page Up
		    	if (top<=0) return;
		    	top -= getNumLines() * lineheight;
		    	if (top < 0) top = 0;
		    	break;
		    case KeyEvent.VK_PAGE_DOWN:		// Page Down
			int l = getNumLines() * lineheight;
		    	if (top+l >= htmlSize.height+borderY) return;
		    	top += l;
		    	break;
		    case KeyEvent.VK_HOME:		// Home
		    	if (top<=0) return;
		    	top = 0;
		    	break;
		    case KeyEvent.VK_END:		// End
		    	top = htmlSize.height+borderY - (getNumLines() * lineheight);
		    	break;
		    default:	return;			// Unrecognized
		}
		repaint();
	}
	public void keyReleased(KeyEvent e) {}
	public void keyTyped(KeyEvent e) {}
	public boolean isFocusTraversable() { return true; }

	// Interface used by HtmlPage to access a ClickListener tag
	public interface ClickListener { public void mouseClicked(HtmlPage p); }

	// Click area information class stored in the clickAreas list
	class ClickArea {
		ClickListener listener;	// Object listening for clicks in this area
		Point start, end;	// Area covered by this ClickListener
		boolean isMulti;	// True if this is a multi-line click area
		ClickArea(ClickListener l, int x, int y) {	// Constructor
			listener = l;				// Set click listener
			start = new Point(x, y);		// Set starting point
			end = null;				// Initialize end point
			isMulti = false;			// Initialize multiline
		}
	}

	// A list of ClickArea objects for tags interested in mouse clicks (eg. links)
	private LinkList clickAreas = new LinkList();		// ClickArea objects

	// Return the currently active click area, or null if none
	private ClickArea getClickArea() {
		int size = clickAreas.size();
		if (size==0) return null;
		ClickArea a = (ClickArea) clickAreas.elementAt(size-1);
		return (a.end==null) ? a : null;
	}

	// Add a ClickArea to the list and store it's starting location
	protected void addClickArea(ClickListener l) {
		if (hidden) return;				// Ignore if hidden
		if (getClickArea()!=null) return;		// Already active?
		ClickArea a = new ClickArea(l, xpos, ypos);	// Create a click area
		clickAreas.addElement(a);			// Add to area list
	}

	// Store the ending location of a ClickArea (or remove it if invisible)
	protected boolean endClickArea() {			// Text area
		return endClickArea(xpos, ypos + getLineHeight(), null);
	}
	private boolean endClickArea(int xpos, int ypos, Graphics g) {
		ClickArea a = getClickArea();			// Get active area
		if (a==null) return false;			// No active area?
		if (!isLineVisible() || (a.start.x==xpos)) {	// Invisible area?
			clickAreas.removeElement(a);		// Remove listener
			return true;				// Abort
		}
		a.end = new Point(xpos, ypos);			// Set ending point
		if (g!=null) {					// Draw area outline?
			Color c = g.getColor();			// Save current colour
			g.setColor(getLinkColor());		// Set link colour
			Point s = a.start, e = a.end;		// Draw a rectangle
			g.drawRect(s.x, s.y, e.x-s.x-1, e.y-s.y-1);
			g.setColor(c);				// Restore colour
		}
		return true;
	}

	// Activate a ClickLister tag if it applies to the location of a mouse event
	private void checkClick(ClickArea a, MouseEvent m) {
		Point s = a.start, e = a.end;			// Get area location
		int x = m.getX() + left, y = m.getY() + top;	// Get click location
		if ((x < s.x) || (x >= e.x)) return;		// Horizontal check
		if ((y < s.y) || (y >= e.y)) return;		// Vertical check
		a.listener.mouseClicked(this);			// Call ClickListener
	}

	// Page constructors
	public HtmlPage(char[] text) { this(text, new HtmlPageReader()); }
	public HtmlPage(char[] text, HtmlReader reader) { this(text, false, reader); }
	public HtmlPage(char[] text, boolean isPlain, HtmlReader reader) {
		this.reader = reader;				// Set HTML reader
		lineBuffer = new DrawStringBuffer();		// Get drawString buffer
		setText(text, isPlain);				// Set text/init reader
		addKeyListener(this);				// Listen for keys

		// Create and override a private ComponentAdapter for resize events
		addComponentListener(new ComponentAdapter() {
			// Metrics update required if component is resized
			public void componentResized(ComponentEvent e) { update = true; }
		});

		// Create and override a private MouseAdapter for mouse clicks
		addMouseListener(new MouseAdapter() {
			// Propogate MouseEvent to all interested ClickListeners
			public void mouseClicked(MouseEvent e) {
				requestFocus();			// Get input focus
				Enumeration a = clickAreas.elements();
				while (a.hasMoreElements())	// Check clickAreas
					checkClick((ClickArea)a.nextElement(), e);
			}
		});
	}

}

Go To: Source Code