/**
* TextBoxView.java
* (c) Peter Bielik and Radek Burget, 2011-2012
*
* SwingBox is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* SwingBox is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with SwingBox. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.fit.cssbox.swingbox.view;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.font.FontRenderContext;
import java.awt.font.TextHitInfo;
import java.awt.font.TextLayout;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.util.List;
import java.util.Map;
import javax.swing.event.DocumentEvent;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Element;
import javax.swing.text.Highlighter;
import javax.swing.text.JTextComponent;
import javax.swing.text.LayeredHighlighter;
import javax.swing.text.Position;
import javax.swing.text.Position.Bias;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.View;
import javax.swing.text.ViewFactory;
import org.fit.cssbox.layout.BlockBox;
import org.fit.cssbox.layout.TextBox;
import org.fit.cssbox.swingbox.util.Anchor;
import org.fit.cssbox.swingbox.util.Constants;
import cz.vutbr.web.css.CSSProperty.FontVariant;
import cz.vutbr.web.css.CSSProperty.TextDecoration;
/**
* The Class TextBoxView. This renders a text.
*
* @author Peter Bielik
* @author Radek Burget
*/
public class TextBoxView extends View implements CSSBoxView
{
private TextBox box;
private Font font;
private Color foreground;
private List<TextDecoration> textDecoration;
private String fontVariant;
private TextLayout layout;
private AffineTransform transform;
private int order;
/** the cache of attributes */
private AttributeSet attributes;
/** decides whether to construct a cache from current working properties */
private boolean refreshAttributes;
private boolean refreshProperties;
private boolean refreshTextLayout;
private boolean underline;
private boolean strike;
private boolean overline;
private Container container;
private Anchor anchor;
/**
* Instantiates a new text based view, able to display rich text. This view
* corresponds to TextBox in CSSBox. <br>
* <a href="http://www.w3.org/TR/CSS21/box.html">Box Model</a>
*
* @param elem
* the elem
*
*/
public TextBoxView(Element elem)
{
super(elem);
AttributeSet tmpAttr = elem.getAttributes();
Object obj = tmpAttr.getAttribute(Constants.ATTRIBUTE_BOX_REFERENCE);
anchor = (Anchor) tmpAttr.getAttribute(Constants.ATTRIBUTE_ANCHOR_REFERENCE);
Integer i = (Integer) tmpAttr.getAttribute(Constants.ATTRIBUTE_DRAWING_ORDER);
order = (i == null) ? -1 : i;
if (obj instanceof TextBox)
{
box = (TextBox) obj;
}
else
{
throw new IllegalArgumentException("Box reference is not an instance of TextBox");
}
if (box.getNode() != null && box.getNode().getParentNode() instanceof org.w3c.dom.Element)
{
org.w3c.dom.Element pelem = Anchor.findAnchorElement((org.w3c.dom.Element) box.getNode().getParentNode());
Map<String, String> elementAttributes = anchor.getProperties();
if (pelem != null)
{
anchor.setActive(true);
elementAttributes.put(Constants.ELEMENT_A_ATTRIBUTE_HREF, pelem.getAttribute("href"));
elementAttributes.put(Constants.ELEMENT_A_ATTRIBUTE_NAME, pelem.getAttribute("name"));
elementAttributes.put(Constants.ELEMENT_A_ATTRIBUTE_TITLE, pelem.getAttribute("title"));
String target = pelem.getAttribute("target");
if ("".equals(target))
{
target = "_self";
}
elementAttributes.put(Constants.ELEMENT_A_ATTRIBUTE_TARGET, target);
// System.err.println("## Anchor at : " + this + " attr: "+
// elementAttributes);
}
else
{
anchor.setActive(false);
elementAttributes.clear();
}
}
}
@Override
public int getDrawingOrder()
{
return order;
}
// --- View methods ---------------------------------------------
@Override
public void setParent(View parent)
{
super.setParent(parent);
if (parent != null)
{
transform = new AffineTransform();
setPropertiesFromAttributes(getElement().getAttributes());
refreshAttributes = true;
refreshProperties = false;
container = getContainer();
/*if (parent instanceof ElementBoxView)
{
// avoid a RootView or any other non-SwingBox views
Anchor parentAnchor = ((ElementBoxView) parent).getAnchor();
if (parentAnchor.isActive())
{
// share elementAttributes
anchor.setActive(true);
anchor.getProperties().putAll(parentAnchor.getProperties());
}
}*/
}
else
{
anchor = null;
transform = null;
container = null;
}
}
@Override
public View createFragment(int p0, int p1)
{
// this method will return THIS object
// -- currently, fragmenting not supported
// we are fragmented by CSSBox !
return this;
}
@Override
public void changedUpdate(DocumentEvent e, Shape a, ViewFactory f)
{
//assume that attributes have changed, reflect changes immediately
invalidateProperties();
syncProperties();
invalidateTextLayout();
super.changedUpdate(e, a, f);
}
@Override
public float getMaximumSpan(int axis)
{
// currently we do not support dynamic sizing, we are pre-computed by CSSBox!
return getPreferredSpan(axis);
}
@Override
public float getPreferredSpan(int axis)
{
// returns total width including margins and borders
switch (axis)
{
case View.X_AXIS:
return 10f; //box.getWidth();
case View.Y_AXIS:
return 10f; //box.getHeight();
default:
throw new IllegalArgumentException("Invalid axis: " + axis);
}
}
@Override
public float getMinimumSpan(int axis)
{
// currently we do not support dynamic sizing, we are pre-computed by CSSBox!
return getPreferredSpan(axis);
}
/**
* Checks if is visible.
*
* @return true, if is visible
*/
@Override
public boolean isVisible()
{
return box.isVisible();
}
@Override
public int getResizeWeight(int axis)
{
// not resizable
return 0;
}
@Override
public Shape modelToView(int pos, Shape a, Bias b)
throws BadLocationException
{
TextLayout layout = getTextLayout();
int offs = pos - getStartOffset(); // the start position this view is responsible for
Rectangle alloc = new Rectangle(toRect(a));
TextHitInfo hit = ((b == Position.Bias.Forward) ? TextHitInfo.afterOffset(offs) : TextHitInfo.beforeOffset(offs));
float[] locs = layout.getCaretInfo(hit);
// hint: nie je lepsie to prepisat na setBounds, ktory berie int ?
alloc.setRect(alloc.getX() + locs[0], alloc.getY(), 1D,
alloc.getHeight());
return alloc;
}
@Override
public int viewToModel(float x, float y, Shape a, Bias[] biasReturn)
{
Rectangle alloc = toRect(a);
// Move the y co-ord of the hit onto the baseline. This is because
// TextLayout supports
// italic carets and we do not.
TextLayout layout = getTextLayout();
TextHitInfo hit = layout.hitTestChar(x - (float) alloc.getX(), 0f);
// TextHitInfo hit = layout.hitTestChar(x - box.getAbsoluteContentX(),
// 0f);
int pos = hit.getInsertionIndex();
biasReturn[0] = hit.isLeadingEdge() ? Position.Bias.Forward
: Position.Bias.Backward;
return pos + getStartOffset();
}
@Override
public AttributeSet getAttributes()
{
if (refreshAttributes)
{
attributes = createAttributes();
refreshAttributes = false;
refreshProperties = false;
}
// always returns the same instance.
// We need to know, if somebody modifies us outside..
return attributes;
}
private AttributeSet createAttributes()
{
// get all 'working variables' and make an AttributeSet.
SimpleAttributeSet res = new SimpleAttributeSet();
res.addAttribute(Constants.ATTRIBUTE_BOX_REFERENCE, box);
res.addAttribute(Constants.ATTRIBUTE_ANCHOR_REFERENCE, anchor);
res.addAttribute(Constants.ATTRIBUTE_FONT_VARIANT, fontVariant);
res.addAttribute(Constants.ATTRIBUTE_TEXT_DECORATION, textDecoration);
res.addAttribute(Constants.ATTRIBUTE_FONT, font);
res.addAttribute(Constants.ATTRIBUTE_FOREGROUND, foreground);
return res;
}
@Override
public String getToolTipText(float x, float y, Shape allocation)
{
if (anchor.isActive())
{
Map<String, String> elementAttributes = anchor.getProperties();
String val = "";
String tmp;
tmp = elementAttributes.get(Constants.ELEMENT_A_ATTRIBUTE_TITLE);
if (tmp != null && !"".equals(tmp))
val = val + "<i>" + tmp + "</i><br>";
tmp = elementAttributes.get(Constants.ELEMENT_A_ATTRIBUTE_HREF);
if (tmp != null && !"".equals(tmp)) val = val + tmp;
return "".equals(val) ? null : "<html>" + val + "</html>";
}
//return "NotLink: " + this;
return null;
}
@Override
public void paint(Graphics gg, Shape a)
{
//System.out.println("Paint text: " + this + " in " + a);
if (isVisible())
{
processPaint(gg, a);
}
}
/**
* Process paint.
*
* @param gg
* the graphics context
* @param a
* the allocation
*/
protected void processPaint(Graphics gg, Shape a)
{
Graphics2D g = (Graphics2D) gg;
AffineTransform tmpTransform = g.getTransform();
if (!tmpTransform.equals(transform))
{
transform = tmpTransform;
invalidateTextLayout();
}
Component c = container;
int p0 = getStartOffset();
int p1 = getEndOffset();
Color fg = getForeground();
if (c instanceof JTextComponent)
{
JTextComponent tc = (JTextComponent) c;
if (!tc.isEnabled())
{
fg = tc.getDisabledTextColor();
}
// javax.swing.plaf.basic.BasicTextUI $ BasicHighlighter
// >> DefaultHighlighter
// >> DefaultHighlightPainter
Highlighter highLighter = tc.getHighlighter();
if (highLighter instanceof LayeredHighlighter)
{
((LayeredHighlighter) highLighter).paintLayeredHighlights(g, p0, p1, box.getAbsoluteContentBounds(), tc, this);
// (g, p0, p1, a, tc, this);
}
}
// nothing is selected
if (!box.isEmpty() && !getText().isEmpty())
renderContent(g, a, fg, p0, p1);
}
/**
* Renders content.
*
* @param g
* the graphics
* @param a
* the allocation
* @param fg
* the color of foreground
* @param p0
* start position
* @param p1
* end position
*/
protected void renderContent(Graphics2D g, Shape a, Color fg, int p0, int p1)
{
TextLayout layout = getTextLayout();
Rectangle absoluteBounds = box.getAbsoluteBounds();
Rectangle absoluteContentBounds = box.getAbsoluteContentBounds();
int pStart = getStartOffset();
int pEnd = getEndOffset();
int x = absoluteBounds.x;
int y = absoluteBounds.y;
Shape oldclip = g.getClip();
BlockBox clipblock = box.getClipBlock();
if (clipblock != null)
{
Rectangle newclip = clipblock.getClippedContentBounds();
Rectangle clip = toRect(oldclip).intersection(newclip);
g.setClip(clip);
}
g.setFont(getFont());
g.setColor(fg);
// -- Draw the string at specified positions --
if (p0 > pStart || p1 < pEnd)
{
try
{
// TextLayout can't render only part of it's range, so if a
// partial range is required, add a clip region.
Shape s = modelToView(p0, Position.Bias.Forward, p1, Position.Bias.Backward, a);
absoluteContentBounds = absoluteContentBounds.intersection(toRect(s));
} catch (BadLocationException ignored)
{
}
}
// render the text
layout.draw(g, x, y + layout.getAscent());
//render the decoration
if (underline || strike || overline)
{
Stroke origStroke = g.getStroke();
int w;
if (getFont().isBold())
w = getFont().getSize() / 8;
else
w = getFont().getSize() / 10;
if (w < 1) w = 1;
y += w / 2;
g.setStroke(new BasicStroke(w));
int xx = absoluteContentBounds.x + absoluteContentBounds.width;
if (overline)
{
g.drawLine(absoluteContentBounds.x, y, xx, y);
}
if (underline)
{
int yy = y + absoluteContentBounds.height - (int) layout.getDescent();
g.drawLine(absoluteContentBounds.x, yy, xx, yy);
}
if (strike)
{
int yy = y + absoluteContentBounds.height / 2;
g.drawLine(absoluteContentBounds.x, yy, xx, yy);
}
g.setStroke(origStroke);
}
g.setClip(oldclip);
}
/**
* Repaints the content, used by blink decoration.
*
* @param ms
* time - the upper bound of delay
* @param bounds
* the bounds
*/
protected void repaint(final int ms, final Rectangle bounds)
{
if (container != null)
{
container.repaint(ms, bounds.x, bounds.y, bounds.width, bounds.height);
}
}
// --- Custom methods -------------------------------------------------
/**
* Gets the string bounds.
*
* @param tl
* textlayout instance
* @return the string bounds
*/
protected Rectangle2D getStringBounds(TextLayout tl)
{
return new Rectangle2D.Float(0, -tl.getAscent(), tl.getAdvance(),
tl.getAscent() + tl.getDescent() + tl.getLeading());
}
/**
* Gets the text.
*
* @return the text
*/
protected String getText()
{
return getText(getStartOffset(), getEndOffset());
}
/**
* Gets the text.
*
* @param p0
* start position
* @param p1
* end position
* @return the text
*/
protected String getText(int p0, int p1)
{
return getTextEx(p0, p1 - p0);
}
/**
* Gets the text.
*
* @param position
* the position, where to begin
* @param len
* the length of text portion
* @return the text
*/
protected String getTextEx(int position, int len)
{
try {
return getDocument().getText(position, len);
} catch (BadLocationException e) {
e.printStackTrace();
return "";
}
}
/**
* Sets the properties from the attributes.
*
* @param attr
* the new properties from attributes
*/
protected void setPropertiesFromAttributes(AttributeSet attr)
{
if (attr != null)
{
Font newFont = (Font) attr.getAttribute(Constants.ATTRIBUTE_FONT);
if (newFont != null)
{
setFont(newFont);
}
else
{
// the font is the most important for us
throw new IllegalStateException("Font can not be null !");
}
setForeground((Color) attr.getAttribute(Constants.ATTRIBUTE_FOREGROUND));
setFontVariant((String) attr.getAttribute(Constants.ATTRIBUTE_FONT_VARIANT));
@SuppressWarnings("unchecked")
List<TextDecoration> attribute = (List<TextDecoration>) attr.getAttribute(Constants.ATTRIBUTE_TEXT_DECORATION);
setTextDecoration(attribute);
}
}
@Override
public String toString()
{
return getText();
}
/**
* Update properties.
*/
public void updateProperties()
{
invalidateProperties();
}
private void invalidateCache()
{
refreshAttributes = true;
}
private void invalidateProperties()
{
refreshProperties = true;
}
private void invalidateTextLayout()
{
refreshTextLayout = true;
}
private void syncProperties()
{
if (refreshProperties)
{
setPropertiesFromAttributes(attributes);
// now, properties == attributes, so no need to refresh something
refreshProperties = false;
refreshAttributes = false;
}
}
/**
* Sets the font.
*
* @param newFont
* the new font
*/
protected void setFont(Font newFont)
{
if (font == null || !font.equals(newFont))
{
font = new Font(newFont.getAttributes());
invalidateCache();
invalidateTextLayout();
}
}
/**
* Sets the foreground.
*
* @param newColor
* the new foreground
*/
protected void setForeground(Color newColor)
{
if (foreground == null || !foreground.equals(newColor))
{
foreground = new Color(newColor.getRGB());
invalidateCache();
}
}
/**
* Sets the font variant.
*
* @param newFontVariant
* the new font variant
*/
protected void setFontVariant(FontVariant newFontVariant)
{
setFontVariant(newFontVariant.toString());
}
/**
* Sets the font variant.
*
* @param newFontVariant
* the new font variant
*/
protected void setFontVariant(String newFontVariant)
{
if (fontVariant == null || !fontVariant.equals(newFontVariant))
{
FontVariant val[] = FontVariant.values();
for (FontVariant aVal : val)
{
if (aVal.toString().equals(newFontVariant))
{
fontVariant = newFontVariant;
invalidateCache();
return;
}
}
}
}
/**
* Sets the text decoration.
*
* @param newTextDecoration
* the new text decoration
*/
protected void setTextDecoration(List<TextDecoration> newTextDecoration)
{
if (textDecoration == null || !textDecoration.equals(newTextDecoration))
{
textDecoration = newTextDecoration;
reflectTextDecoration(textDecoration);
invalidateCache();
}
}
private void reflectTextDecoration(List<TextDecoration> decor)
{
underline = false;
strike = false;
overline = false;
for (TextDecoration aDecor : decor)
{
if (TextDecoration.UNDERLINE == aDecor)
{
underline = true;
}
else if (TextDecoration.LINE_THROUGH == aDecor)
{
strike = true;
}
else if (TextDecoration.OVERLINE == aDecor)
{
overline = true;
}
}
}
/**
* Gets the text layout.
*
* @return the text layout
*/
protected TextLayout getTextLayout()
{
if (refreshTextLayout)
{
refreshTextLayout = false;
layout = new TextLayout(getText(), getFont(), new FontRenderContext(transform, true, false));
}
return layout;
}
/**
* Gets the font.
*
* @return the font
*/
public Font getFont()
{
syncProperties();
return font;
}
/**
* Gets the foreground.
*
* @return the foreground
*/
public Color getForeground()
{
syncProperties();
return foreground;
}
/**
* Gets the font variant.
*
* @return the font variant
*/
public String getFontVariant()
{
syncProperties();
return fontVariant;
}
/**
* Gets the text decoration.
*
* @return the text decoration
*/
public List<TextDecoration> getTextDecoration()
{
syncProperties();
return textDecoration;
}
/**
* converts a shape to rectangle
*
* @param a
* the allocation shape
* @return the rectangle
*/
public static final Rectangle toRect(Shape a)
{
return a instanceof Rectangle ? (Rectangle) a : a.getBounds();
}
}