/** * 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(); } }