Revision: 642 Author: allain.lalonde Date: Tue Aug 4 07:26:15 2009 Log: issue#111: merged phtml branch to trunk http://code.google.com/p/piccolo2d/source/detail?r=642
Added: /piccolo2d.java/trunk/core/src/main/java/edu/umd/cs/piccolo/nodes/PHtmlView.java /piccolo2d.java/trunk/core/src/test/java/edu/umd/cs/piccolo/nodes/PHtmlViewTest.java /piccolo2d.java/trunk/examples/src/main/java/edu/umd/cs/piccolo/examples/HtmlViewExample.java Modified: /piccolo2d.java/trunk/examples/src/main/java/edu/umd/cs/piccolo/examples/ExampleRunner.java ======================================= --- /dev/null +++ /piccolo2d.java/trunk/core/src/main/java/edu/umd/cs/piccolo/nodes/PHtmlView.java Tue Aug 4 07:26:15 2009 @@ -0,0 +1,442 @@ +/* + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolo.nodes; + +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; + +import javax.swing.JLabel; +import javax.swing.JTextField; +import javax.swing.plaf.basic.BasicHTML; +import javax.swing.text.Position; +import javax.swing.text.View; + +import edu.umd.cs.piccolo.PNode; +import edu.umd.cs.piccolo.util.PPaintContext; + +/** + * PHtml is a Piccolo node for rendering HTML text. It uses a JLabel under the + * hood so you have the same restrictions regarding html as you have when using + * standard Swing components (HTML 3.2 + subset of CSS 1.0). + * + * @author Chris Malley (cmal...@pixelzoom.com) + * @author Sam Reid + * @author Allain Lalonde + */ +public class PHtmlView extends PNode { + + private static final long serialVersionUID = 1L; + + /** Default font to use if not overridden in the HTML markup. */ + private static final Font DEFAULT_FONT = new JTextField().getFont(); + + /** Default font color to use if not overridden in the HTML markup */ + private static final Color DEFAULT_HTML_COLOR = Color.BLACK; + + /** + * The property name that identifies a change of this node's font (see + * {...@link #getFont getFont}). Both old and new value will be set in any + * property change event. + */ + public static final String PROPERTY_FONT = "font"; + + /** + * The property code that identifies a change of this node's font (see + * {...@link #getFont getFont}). Both old and new value will be set in any + * property change event. + */ + public static final int PROPERTY_CODE_FONT = 1 << 20; + + /** + * The property name that identifies a change of this node's HTML (see + * {...@link #getHTML getHTML}). Both old and new value will be set in any + * property change event. + */ + public static final String PROPERTY_HTML = "html"; + + /** + * The property code that identifies a change of this node's HTML (see + * {...@link #getHTML getHTML}). Both old and new value will be set in any + * property change event. + */ + public static final int PROPERTY_CODE_HTML = 1 << 21; + + /** + * The property name that identifies a change of this node's HTML color (see + * {...@link #getHtml getHTMLColor}). Both old and new value will be set in any + * property change event. + */ + public static final String PROPERTY_HTML_COLOR = "html color"; + + /** + * The property code that identifies a change of this node's HTML color (see + * {...@link #getHtml getHTMLColor}). Both old and new value will be set in any + * property change event. + */ + public static final int PROPERTY_CODE_HTML_COLOR = 1 << 22; + + /** Underlying JLabel used to handle the rendering logic. */ + private final JLabel htmlLabel; + + /** Object that encapsulates the HTML rendering logic. */ + private View htmlView; + + /** Used to enforce bounds and wrapping on the HTML. */ + private final Rectangle htmlBounds; + + /** + * Creates an empty PHtml node with default font and color. + */ + public PHtmlView() { + this(null, DEFAULT_FONT, DEFAULT_HTML_COLOR); + } + + /** + * Creates a PHtml node that contains the provided HTML. It will have + * default font and color. + * + * @param html markup label should contain + */ + public PHtmlView(final String html) { + this(html, DEFAULT_FONT, DEFAULT_HTML_COLOR); + } + + /** + * Creates a PHtml node with the given markup and default html color. + * + * @param html markup label should contain + * @param htmlColor color that will be used unless overridden by the markup + */ + public PHtmlView(final String html, final Color htmlColor) { + this(html, DEFAULT_FONT, htmlColor); + } + + /** + * Creates a PHtml node with the given markup and default HTML color. + * + * @param html markup label should contain + * @param font font that will be used unless overriden by the markup + * @param htmlColor color that will be used unless overridden by the markup + */ + public PHtmlView(final String html, final Font font, final Color htmlColor) { + htmlLabel = new JLabel(html); + htmlLabel.setFont(font); + htmlLabel.setForeground(htmlColor); + htmlBounds = new Rectangle(); + update(); + } + + /** + * @return HTML being rendered by this node + */ + public String getHtml() { + return htmlLabel.getText(); + } + + /** + * Changes the HTML being rendered by this node. + * + * @param newHtml markup to swap with existing HTML + */ + public void setHtml(final String newHtml) { + if (isNewHtml(newHtml)) { + final String oldHtml = htmlLabel.getText(); + htmlLabel.setText(newHtml); + update(); + firePropertyChange(PROPERTY_CODE_HTML, PROPERTY_HTML, oldHtml, newHtml); + } + } + + private boolean isNewHtml(final String html) { + if (html == null && getHtml() == null) { + return false; + } + else if (html == null || getHtml() == null) { + return true; + } + else { + return !htmlLabel.getText().equals(html); + } + } + + /** + * Returns the default font being used when not overridden in the markup. + * + * @return font being used when not overridden by the markup + */ + public Font getFont() { + return htmlLabel.getFont(); + } + + /** + * Set the font of this PHtml. This may be overridden by the markup using + * either styles or the font tag. + * + * @param newFont font to set as the default + */ + public void setFont(final Font newFont) { + final Font oldFont = htmlLabel.getFont(); + htmlLabel.setFont(newFont); + update(); + + firePropertyChange(PROPERTY_CODE_FONT, PROPERTY_FONT, oldFont, newFont); + } + + /** + * Gets the color used to render the HTML. If you want to get the paint used + * for the node, use getPaint. + * + * @return the color used to render the HTML. + */ + public Color getHtmlColor() { + return htmlLabel.getForeground(); + } + + /** + * Sets the color used to render the HTML. If you want to set the paint used + * for the node, use setPaint. + * + * @param newColor new color to use when rendering HTML + */ + public void setHtmlColor(final Color newColor) { + final Color oldColor = htmlLabel.getForeground(); + htmlLabel.setForeground(newColor); + repaint(); + firePropertyChange(PROPERTY_CODE_HTML_COLOR, PROPERTY_HTML_COLOR, oldColor, newColor); + } + + /** + * Applies all properties to the underlying JLabel, creates an htmlView and + * updates bounds + */ + private void update() { + htmlLabel.setSize(htmlLabel.getPreferredSize()); + htmlView = BasicHTML.createHTMLView(htmlLabel, htmlLabel.getText() == null ? "" : htmlLabel.getText()); + + final Rectangle2D bounds = getBounds(); + htmlBounds.setRect(0, 0, bounds.getWidth(), bounds.getHeight()); + repaint(); + } + + /** {...@inheritdoc} */ + public boolean setBounds(final double x, final double y, final double width, final double height) { + final boolean boundsChanged = super.setBounds(x, y, width, height); + update(); + return boundsChanged; + } + + /** {...@inheritdoc} */ + public boolean setBounds(final Rectangle2D newBounds) { + final boolean boundsChanged = super.setBounds(newBounds); + update(); + return boundsChanged; + } + + /** + * Paints the node. The HTML string is painted last, so it appears on top of + * any child nodes. + * + * @param paintContext the context in which painting is occurring + */ + protected void paint(final PPaintContext paintContext) { + super.paint(paintContext); + + if (htmlLabel.getWidth() != 0 && htmlLabel.getHeight() != 0) { + final Graphics2D g2 = paintContext.getGraphics(); + + htmlView.paint(g2, htmlBounds); + } + } + + /** + * Returns the address specified in the link under the given point. + * + * @param clickedPoint + * @return String containing value of href for clicked link, or null if no + * link clicked + */ + public String getClickedAddress(final Point2D.Double clickedPoint) { + return getClickedAddress(clickedPoint.getX(), clickedPoint.getY()); + } + + /** + * Returns the address specified in the link under the given point. + * + * @param clickedPoint + * @return String containing value of href for clicked link, or null if no + * link clicked + */ + public String getClickedAddress(final double x, final double y) { + int position = pointToModelIndex(x, y); + + final String html = htmlLabel.getText(); + + String address = null; + + int currentPos = 0; + while (currentPos < html.length()) { + currentPos = html.indexOf('<', currentPos); + if (currentPos == -1 || position < currentPos) { + break; + } + + final int tagStart = currentPos; + final int tagEnd = findTagEnd(html, currentPos); + + if (tagEnd == -1) { + return null; + } + + currentPos = tagEnd + 1; + + final String tag = html.substring(tagStart, currentPos); + + position += tag.length(); + + if ("</a>".equals(tag)) { + address = null; + } + else if (tag.startsWith("<a ")) { + address = extractHref(tag); + } + } + + return address; + } + + /** + * Returns the index into the raw text (without HTML) that the click + * occurred. + * + * @param x x component of the point clicked + * @param y y component of the point clicked + * @return index into the raw text (without HTML) that the click occurred + */ + private int pointToModelIndex(final double x, final double y) { + final Position.Bias[] biasReturn = new Position.Bias[1]; + return htmlView.viewToModel((float) x, (float) y, getBounds(), biasReturn); + } + + /** + * Starting from the startPos, it finds the position at which the given tag + * ends. Returns -1 if the end of the string was encountered before the end + * of the tag was encountered. + * + * @param html raw HTML string being searched + * @param startPos where in the string to start searching for ">" + * @return index after the ">" character + */ + private int findTagEnd(final String html, final int startPos) { + int currentPos = startPos; + + currentPos++; + + while (currentPos > 0 && currentPos < html.length() && html.charAt(currentPos) != '>') { + if (html.charAt(currentPos) == '\"') { + currentPos = html.indexOf('\"', currentPos + 1); + } + else if (html.charAt(currentPos) == '\'') { + currentPos = html.indexOf('\'', currentPos + 1); + } + currentPos++; + } + + return currentPos == 0 || currentPos >= html.length() ? -1 : currentPos + 1; + } + + /** + * Given a tag, extracts the value of the href attribute, returns null if + * none was found. + * + * @param tag from which to extract the href value + * @return href value without quotes or null if not found + */ + private String extractHref(final String tag) { + int currentPos = 0; + + final String href = null; + + while (currentPos >= 0 && currentPos < tag.length() - 1) { + currentPos = tag.indexOf('=', currentPos + 1); + if (currentPos != -1 && isHrefAttributeAssignment(tag, currentPos)) { + return extractHrefValue(tag, currentPos + 1); + } + } + return href; + } + + /** + * Starting at the character after the equal sign of an href=..., it extract + * the value. Handles single, double, and no quotes. + * + * @param tag + * @param startPos + * @return value of href or null if not found. + */ + private String extractHrefValue(final String tag, final int startPos) { + int currentPos = startPos; + + if (tag.charAt(currentPos) == '\"') { + final int startHref = currentPos + 1; + currentPos = tag.indexOf('\"', startHref); + return currentPos == -1 ? null : tag.substring(startHref, currentPos); + } + else if (currentPos < tag.length() && tag.charAt(currentPos) == '\'') { + final int startHref = currentPos + 1; + currentPos = tag.indexOf('\'', startHref); + return currentPos == -1 ? null : tag.substring(startHref, currentPos); + } + else { + final int startHref = currentPos; + + if (currentPos < tag.length()) { + do { + currentPos++; + } while (currentPos < tag.length() && tag.charAt(currentPos) != ' ' && tag.charAt(currentPos) != '>'); + } + return tag.substring(startHref, currentPos); + } + } + + /** + * Given the position in a string returns whether it points to the equal + * sign of an href attribute. + * + * @param tag html code of the tag + * @param equalPos the index of the assignment + * + * @return true if to left of assignment is href + */ + private boolean isHrefAttributeAssignment(final String tag, final int equalPos) { + return tag.charAt(equalPos) == '=' && equalPos > 4 && " href".equals(tag.substring(equalPos - 5, equalPos)); + } +} ======================================= --- /dev/null +++ /piccolo2d.java/trunk/core/src/test/java/edu/umd/cs/piccolo/nodes/PHtmlViewTest.java Tue Aug 4 07:26:15 2009 @@ -0,0 +1,128 @@ +package edu.umd.cs.piccolo.nodes; + +import java.awt.Color; +import java.awt.Font; + +import junit.framework.TestCase; +import edu.umd.cs.piccolo.MockPropertyChangeListener; +import edu.umd.cs.piccolo.util.PBounds; + +public class PHtmlViewTest extends TestCase { + + private MockPropertyChangeListener mockListener; + + public void setUp() { + mockListener = new MockPropertyChangeListener(); + } + + public void testGetClickedAddressReturnsSingleQuotedAddress() { + PHtmlView html = new PHtmlView("<a href='http://www.testing.com'>testing</a>"); + html.setBounds(new PBounds(0, 0, 100, 100)); + assertEquals("http://www.testing.com", html.getClickedAddress(5,5)); + } + + public void testGetClickedAddressReturnsDoubleQuotedAddress() { + PHtmlView html = new PHtmlView("<a href=\"http://www.testing.com\">testing</a>"); + html.setBounds(new PBounds(0, 0, 100, 100)); + assertEquals("http://www.testing.com", html.getClickedAddress(5,5)); + } + + public void testBracketsAreValidInHrefs() { + PHtmlView html = new PHtmlView("<a href='a>b'>testing</a>"); + html.setBounds(new PBounds(0, 0, 100, 100)); + assertEquals("a>b", html.getClickedAddress(5,5)); + } + + public void testGetClickedAddressReturnsNullWhenInvalid() { + PHtmlView html = new PHtmlView("<a ='a>b'>testing</a>"); + html.setBounds(new PBounds(0, 0, 100, 100)); + assertNull(html.getClickedAddress(5,5)); + } + + public void testGetClickedAddressReturnsHrefWhenMissingEndAnchorTag() { + PHtmlView html = new PHtmlView("<a href='testing.com'>testing"); + html.setBounds(new PBounds(0, 0, 100, 100)); + assertEquals("testing.com", html.getClickedAddress(5,5)); + } + + public void testHandlesTricksyTitles() { + PHtmlView html = new PHtmlView("<a href=\"where to go\" title=\"this is not the href='gotcha!' \">testing</a>"); + html.setBounds(new PBounds(0, 0, 100, 100)); + assertEquals("where to go", html.getClickedAddress(5,5)); + } + + public void testHandlesHrefWithoutQuotes() { + PHtmlView html = new PHtmlView("<a href=testing.com>testing</a>"); + html.setBounds(new PBounds(0, 0, 100, 100)); + assertEquals("testing.com", html.getClickedAddress(5,5)); + } + + public void testUnclosedTagsCauseIgnoreOfTag() { + PHtmlView html = new PHtmlView("<a href='testing.com' "); + html.setBounds(new PBounds(0, 0, 100, 100)); + assertNull(html.getClickedAddress(5,5)); + } + + public void testMissingEndTagCausesRemainderOfHtmlToBeLinkTarget() { + PHtmlView html = new PHtmlView("<a href='testing.com'>Missing End TAg "); + html.setBounds(new PBounds(0, 0, 100, 100)); + assertEquals("testing.com", html.getClickedAddress(5,5)); + } + + public void testUnclosedQuotesCauseIgnoreOfLink() { + PHtmlView html = new PHtmlView("<a href='testing.com>testing"); + html.setBounds(new PBounds(0, 0, 100, 100)); + assertNull(html.getClickedAddress(5,5)); + } + + public void testEmptyAddressReturnsEmptyString() { + PHtmlView html = new PHtmlView("<a href=''>testing"); + html.setBounds(new PBounds(0, 0, 100, 100)); + assertEquals("", html.getClickedAddress(5,5)); + } + + public void testReturnsNullWhenClickOutsideLink() { + PHtmlView html = new PHtmlView("0123456789 <a href=#>testing</a>"); + html.setBounds(new PBounds(0, 0, 100, 100)); + assertNull(html.getClickedAddress(5,5)); + } + + public void testSetHtmlColorPersists() { + PHtmlView html = new PHtmlView(); + html.setHtmlColor(Color.RED); + assertEquals(Color.RED, html.getHtmlColor()); + } + + public void testFontIsNotNullByDefault() { + PHtmlView html = new PHtmlView(); + assertNotNull(html.getFont()); + } + + public void testHtmlColorIsNotNullByDefault() { + PHtmlView html = new PHtmlView(); + assertNotNull(html.getHtmlColor()); + } + + public void testSetHtmlFiresEventOnChangeOnly() { + PHtmlView html = new PHtmlView(); + html.addPropertyChangeListener(mockListener); + html.setHtml("testing"); + assertEquals(1, mockListener.getPropertyChangeCount()); + assertEquals(PHtmlView.PROPERTY_HTML, mockListener.getPropertyChange(0).getPropertyName()); + html.setHtml("testing"); + assertEquals(1, mockListener.getPropertyChangeCount()); + } + + public void testSetHtmlToNullIsAllowed() { + PHtmlView html = new PHtmlView(); + html.setHtml(null); + assertNull(html.getHtml()); + } + + public void testSetFontPerists() { + PHtmlView html = new PHtmlView(); + Font font = Font.getFont("arial"); + html.setFont(font); + assertSame(font, html.getFont()); + } +} ======================================= --- /dev/null +++ /piccolo2d.java/trunk/examples/src/main/java/edu/umd/cs/piccolo/examples/HtmlViewExample.java Tue Aug 4 07:26:15 2009 @@ -0,0 +1,74 @@ +package edu.umd.cs.piccolo.examples; + +import java.awt.geom.Point2D; + +import javax.swing.JOptionPane; + +import edu.umd.cs.piccolo.PCanvas; +import edu.umd.cs.piccolo.PNode; +import edu.umd.cs.piccolo.event.PBasicInputEventHandler; +import edu.umd.cs.piccolo.event.PInputEvent; +import edu.umd.cs.piccolo.nodes.PHtmlView; +import edu.umd.cs.piccolox.PFrame; + +public class HtmlViewExample extends PFrame { + private static final long serialVersionUID = 1L; + private StringBuffer html; + + public HtmlViewExample() { + this(null); + } + + public HtmlViewExample(final PCanvas aCanvas) { + super("HTMLExample", false, aCanvas); + } + + public void initialize() { + html = new StringBuffer(); + html.append("<p style='margin-bottom: 10px;'>"); + html.append("This is an example <a href='#testing'>of what can</a> be done with PHtml."); + html.append("</p>"); + html.append("<p>It supports:</p>"); + appendFeatures(); + + final PHtmlView htmlNode = new PHtmlView(html.toString()); + htmlNode.setBounds(0, 0, 400, 400); + getCanvas().getLayer().addChild(htmlNode); + + getCanvas().addInputEventListener(new PBasicInputEventHandler() { + public void mouseClicked(final PInputEvent event) { + final PNode clickedNode = event.getPickedNode(); + if (!(clickedNode instanceof PHtmlView)) { + return; + } + + final Point2D clickPoint = event.getPositionRelativeTo(clickedNode); + final PHtmlView htmlNode = (PHtmlView) clickedNode; + + final String url = htmlNode.getClickedAddress(clickPoint); + JOptionPane.showMessageDialog(null, url); + } + }); + } + + private void appendFeatures() { + html.append("<ul>"); + html.append("<li><b>HTML</b> 3.2</li>"); + html.append("<li><font style='color:red; font-style: italic;'>Limited CSS 1.0</font></li>"); + html.append("<li>Tables:"); + appendTable(); + html.append("</li>"); + html.append("</ul>"); + } + + private void appendTable() { + html.append("<table border='1' cellpadding='2' cellspacing='0'>"); + html.append("<tr><th>Col 1</th><th>Col 2</th></tr>"); + html.append("<tr><td>Col 1 val</td><td>Col 2 val</td></tr>"); + html.append("</table>"); + } + + public static void main(final String[] args) { + new HtmlViewExample(); + } +} ======================================= --- /piccolo2d.java/trunk/examples/src/main/java/edu/umd/cs/piccolo/examples/ExampleRunner.java Tue Jul 28 12:46:54 2009 +++ /piccolo2d.java/trunk/examples/src/main/java/edu/umd/cs/piccolo/examples/ExampleRunner.java Tue Aug 4 07:26:15 2009 @@ -107,13 +107,13 @@ BirdsEyeViewExample.class, CameraExample.class, CenterExample.class, ChartLabelExample.class, ClipExample.class, CompositeExample.class, DynamicExample.class, EventHandlerExample.class, FullScreenNodeExample.class, GraphEditorExample.class, GridExample.class, GroupExample.class, - HandleExample.class, HelloWorldExample.class, HierarchyZoomExample.class, KeyEventFocusExample.class, - LayoutExample.class, LensExample.class, NavigationExample.class, NodeCacheExample.class, - NodeEventExample.class, NodeExample.class, NodeLinkExample.class, PanToExample.class, - PathExample.class, PositionExample.class, PositionPathActivityExample.class, PulseExample.class, - ScrollingExample.class, SelectionExample.class, SquiggleExample.class, StickyExample.class, - StickyHandleLayerExample.class, StrokeExample.class, TextExample.class, TooltipExample.class, - TwoCanvasExample.class, WaitForActivitiesExample.class }); + HandleExample.class, HelloWorldExample.class, HierarchyZoomExample.class, HtmlViewExample.class, + KeyEventFocusExample.class, LayoutExample.class, LensExample.class, NavigationExample.class, + NodeCacheExample.class, NodeEventExample.class, NodeExample.class, NodeLinkExample.class, + PanToExample.class, PathExample.class, PositionExample.class, PositionPathActivityExample.class, + PulseExample.class, ScrollingExample.class, SelectionExample.class, SquiggleExample.class, + StickyExample.class, StickyHandleLayerExample.class, StrokeExample.class, TextExample.class, + TooltipExample.class, TwoCanvasExample.class, WaitForActivitiesExample.class }); } private void addExampleButtons(final JPanel panel, final Class[] exampleClasses) { --~--~---------~--~----~------------~-------~--~----~ Piccolo2D Developers Group: http://groups.google.com/group/piccolo2d-dev?hl=en -~----------~----~----~----~------~----~------~--~---