All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.fit.cssbox.layout.TextBox Maven / Gradle / Ivy

Go to download

CSSBox is an (X)HTML/CSS rendering engine written in pure Java. Its primary purpose is to provide a complete information about the rendered page suitable for further processing. However, it also allows displaying the rendered document.

There is a newer version: 5.0.2
Show newest version
/*
 * TextBox.java
 * Copyright (c) 2005-2007 Radek Burget
 *
 * CSSBox 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.
 *  
 * CSSBox 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 CSSBox. If not, see .
 *
 * Created on 5. �nor 2006, 13:42
 */

package org.fit.cssbox.layout;

import java.awt.Color;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.font.TextAttribute;
import java.text.AttributedString;

import org.w3c.dom.Text;

import cz.vutbr.web.css.CSSProperty;
import cz.vutbr.web.css.CSSProperty.TextTransform;

/**
 * A box that corresponds to a text node.
 *
 * @author  radek
 */
public class TextBox extends Box implements Inline
{
    /** Assigned text node */
    protected Text textNode;
    
    /** Text string after whitespace processing */
    protected String text;
    
    /** The start index of the text substring to be displayed */
    protected int textStart;

    /** The end index of the text substring to be displayed (excl) */
    protected int textEnd;

    /** Maximal total width */
    protected int maxwidth;
    
    /** Minimal total width */
    protected int minwidth;
    
    /** Indicates whether to ignore initial whitespaces */
    protected boolean ignoreinitialws;
    
    /** Indicates whether to collapse whitespaces at all */
    protected boolean collapsews;
    
    /** Indicates whether it is allowed to split on whitespace */
    protected boolean splitws;
    
    /** Indicatew whether it line feeds should be treated as whitespace */
    protected boolean linews;
    
    /** When line feeds are preserved, this contains the maximal length of the first line of the text. */
    protected int firstLineLength;
    
    /** When line feeds are preserved, this contains the maximal length of the last line of the text. */
    protected int lastLineLength;
    
    /** Contains the maximal length of the longest line of the text. */
    protected int longestLineLength;
    
    /** Contains a preserved line break? */
    protected boolean containsLineBreak;
    
    /** Layout finished with a line break? */
    protected boolean lineBreakStop;
    
    /** Used text transformation */
    protected CSSProperty.TextTransform transform;
    
    
    //===================================================================
    
    /**
     * Creates a new TextBox formed by a DOM text node.
     * @param n the corresponding DOM text node
     * @param g the graphics context used for rendering
     * @param ctx current visual context
     */
    public TextBox(Text n, Graphics2D g, VisualContext ctx)
    {
        super(n, g, ctx);
        textNode = n;
        transform = TextTransform.NONE;
        setWhiteSpace(ElementBox.WHITESPACE_NORMAL); //resets the text content and indices
        
        ctx.updateForGraphics(null, g);

        ignoreinitialws = false;
        collapsews = true;
        containsLineBreak = false;
        lineBreakStop = false;
    }

    /**
     * Copy all the values from another text box.
     * @param src the source text box
     */
    public void copyValues(TextBox src)
    {
        super.copyValues(src);
        text = new String(src.text);
        ignoreinitialws = false; //only the first box should ignore
        collapsews = src.collapsews;
        splitws = src.splitws;
        linews = src.linews;
        firstLineLength = src.firstLineLength;
        lastLineLength = src.lastLineLength;
        longestLineLength = src.longestLineLength;
        containsLineBreak = src.containsLineBreak;
        transform = src.transform;
    }
    
    /** 
     * Create a new box from the same DOM node in the same context
     * @return the new TextBox 
     */
    public TextBox copyTextBox()
    {
        TextBox ret = new TextBox(textNode, g, ctx);
        ret.copyValues(this);
        return ret;
    }
    
    public String toString()
    {
        return "Text: " + text + "<" + textStart + "," + textEnd + ">";
    }
    
    @Override
    public void initBox()
    {
    }
    
    @Override
    public void setParent(ElementBox parent)
    {
        super.setParent(parent);
        if (getParent() != null)
        {
            //load the relevant style values
            transform = getParent().getStyle().getProperty("text-transform");
            if (transform == null)
                transform = TextTransform.NONE;
            //reset the whitespace processing according to the parent settings
            CSSProperty.WhiteSpace ws = getParent().getWhiteSpace();
            if (ws != ElementBox.WHITESPACE_NORMAL || transform != TextTransform.NONE)
                setWhiteSpace(ws);
        }
    }

    /**
     * @return the text contained in this box
     */
    @Override
    public String getText()
    {
        if (text != null)
            return text.substring(textStart, textEnd);
        else
            return "";
    }

    @Override
    public boolean isDeclaredVisible()
    {
        return true;
    }

    @Override
    public boolean isDisplayed()
    {
        if (getParent() == null)
            return true;
        else
            return parent.isDisplayed();
    }

    @Override
    public boolean isVisible()
    {
        if (getParent() == null)
            return true;
        else
            return parent.isDeclaredVisible() && super.isVisible();
    }
    
    //=======================================================================

    /**
     * Sets the whitespace processing for this box. The text content is then treated accordingly. The text start and text end
     * indices are reset to their initial values.
     */
    public void setWhiteSpace(CSSProperty.WhiteSpace value)
    {
        splitws = (value == ElementBox.WHITESPACE_NORMAL || value == ElementBox.WHITESPACE_PRE_WRAP || value== ElementBox.WHITESPACE_PRE_LINE);
        collapsews = (value == ElementBox.WHITESPACE_NORMAL || value == ElementBox.WHITESPACE_NOWRAP || value == ElementBox.WHITESPACE_PRE_LINE);
        linews = (value == ElementBox.WHITESPACE_NORMAL || value == ElementBox.WHITESPACE_NOWRAP);
        //When this is the original box, apply the whitespace. For the copied boxes, the whitespace has been already applied (they contain
        //a copy of the original, already processed content). 
        if (!splitted)
            applyWhiteSpace();
        //recompute widths (possibly different wrapping)
        computeLineLengths();
        minwidth = computeMinimalWidth();
        maxwidth = computeMaximalWidth();
    }
    
    /**
     * Applies the whitespace processing represented by the {@link #collapsews} property to the text content. The text start and text end
     * indices are reset to their initial values.
     */
    private void applyWhiteSpace()
    {
        text = applyTransformations(collapseWhitespaces(node.getNodeValue()));
        textStart = 0;
        textEnd = text.length();
        isempty = (textEnd == 0);
    }
    
    /**
     * Applies the whitespace removal rules used in HTML.
     * @param src source string
     * @return a new string with additional whitespaces removed
     */
    private String collapseWhitespaces(String src)
    {
        StringBuffer ret = new StringBuffer();
        boolean inws = false;
        for (int i = 0; i < src.length(); i++)
        {
            char ch = src.charAt(i);
            if (collapsews && isWhitespace(ch))
            {
                if (!inws)
                {
                    ret.append(' ');
                    inws = true;
                }
            }
            else if (isLineBreak(ch))
            {
            	ret.append('\r'); //represent all line breaks as LF
            	//reduce eventual CR+LF to CR only
            	if (ch == '\r' && i+1 < src.length() && src.charAt(i+1) == '\n')
            		i++;
            }
            else
            {
                inws = false;
                ret.append(ch);
            }
        }
        return new String(ret);
    }

    /**
     * Applies the text transformations to a string according to the current style.
     * @param src The source string
     * @return the string after transformations 
     */
    private String applyTransformations(String src)
    {
        switch (transform)
        {
            case LOWERCASE:
                return src.toLowerCase();
            case UPPERCASE:
                return src.toUpperCase();
            case CAPITALIZE:
                StringBuilder ret = new StringBuilder(src.length());
                boolean ws = true;
                for (int i = 0; i < src.length(); i++)
                {
                    char ch = src.charAt(i);
                    if (Character.isWhitespace(ch))
                        ws = true;
                    else
                    {
                        if (ws)
                            ch = Character.toUpperCase(ch);
                        ws = false;
                    }
                    ret.append(ch);
                }
                return ret.toString();
            default:
                return src;
        }
    }
    
    /**
     * Removes the trailing whitespaces from the cotnained text string. This must be done before the layout (it resets the text start and end pointers). 
     */
    public void removeTrailingWhitespaces()
    {
        int last = -1;
        for (int i = text.length() - 1; i >= 0; i--)
        {
            if (isWhitespace(text.charAt(i)))
                last = i;
            else
                break;
        }
        if (last != -1)
        {
            text = text.substring(0, last);
            textStart = 0;
            textEnd = last;
        }
    }
    
    /**
     * @return the start offset in the text string
     */ 
    protected int getTextStart()
    {
        return textStart;
    }
    
    /**
     * @param index the start offset in the text string
     */ 
    protected void setTextStart(int index)
    {
        textStart = index;
    }
    
    /**
     * @return the end offset in the text string (not included)
     */ 
    protected int getTextEnd()
    {
        return textEnd;
    }
    
    /**
     * @param index the end offset in the text string (not included)
     */ 
    protected void setTextEnd(int index)
    {
        textEnd = index;
    }
    
	@Override
    public boolean affectsDisplay()
    {
        return !isEmpty();
    }
    
	@Override
    public boolean isWhitespace()
    {
		//after wihtespace processing, all whitespaces should be represented by ' '
		String s = getText();
		for (int i = 0; i < s.length(); i++)
			if (s.charAt(i) != ' ')
				return false;
		return true;
    }
    
    @Override
    public boolean collapsesSpaces()
    {
        return collapsews;
    }
    
    @Override
    public boolean preservesLineBreaks()
    {
        return !linews;
    }

    @Override
    public boolean allowsWrapping()
    {
        return splitws;
    }

    @Override
    public int getContentX() 
    {
        return bounds.x;
    }
    
	@Override
    public int getAbsoluteContentX() 
    {
        return absbounds.x;
    }
    
	@Override
    public int getContentY() 
    {
        return bounds.y;
    }

	@Override
    public int getAbsoluteContentY() 
    {
        return absbounds.y;
    }

	@Override
    public int getContentWidth() 
    {
        return bounds.width;
    }
    
	@Override
    public int getContentHeight() 
    {
        return bounds.height;
    }

	@Override
    public int getAvailableContentWidth() 
    {
        return availwidth;
    }
    
    public int getLineHeight()
    {
        return parent.getLineHeight();
    }
    
    public int getMaxLineHeight()
    {
        return parent.getLineHeight();
    }
    
    public int getTotalLineHeight()
    {
        return ctx.getFontHeight();
    }

    public int getBaselineOffset()
    {
        return ctx.getBaselineOffset();
    }
    
    public int getBelowBaseline()
    {
        return ctx.getFontHeight() - ctx.getBaselineOffset();
    }
    
    public int getHalfLead()
    {
        return 0;
    }
    
    @Override
    public int totalHeight() 
    {
        return bounds.width;
    }
    
	@Override
    public int totalWidth() 
    {
        return bounds.height;
    }
    
	@Override
    public Rectangle getMinimalAbsoluteBounds()
    {
    	return absbounds;
    }
    
	@Override
    public boolean isInFlow()
	{
		return true;
	}
	
	@Override
	public boolean containsFlow()
	{
		return false;
	}
	
    @Override
    public boolean canSplitInside()
    {
        return (getText().indexOf(' ') != -1);
    }
    
    @Override
    public boolean canSplitBefore()
    {
        if (textEnd > textStart)
        	return (text.charAt(textStart) == ' ' ||
        			(textStart > 0 && text.charAt(textStart-1) == ' '));
        else
        	return false;
    }
    
    @Override
    public boolean canSplitAfter()
    {
        if (textEnd > textStart)
	        return (text.charAt(textEnd-1) == ' ' ||
	                (textEnd < text.length() && text.charAt(textEnd) == ' '));
        else
        	return false;
    }
    
    @Override
    public boolean startsWithWhitespace()
    {
        if (textEnd > textStart)
            return (Character.isWhitespace(text.charAt(textStart)));
        else
            return false;
    }
    
    @Override
    public boolean endsWithWhitespace()
    {
        if (textEnd > textStart)
            return (Character.isWhitespace(text.charAt(textEnd-1)));
        else
            return false;
    }
    
    @Override
    public void setIgnoreInitialWhitespace(boolean b)
    {
        this.ignoreinitialws = b;
    }
    
	@Override
	public boolean hasFixedWidth()
	{
		return false; //the width depends on the content
	}

	@Override
	public boolean hasFixedHeight()
	{
		return false;
	}

    //=======================================================================
    
	/** 
	 * Compute the width and height of this element. Layout the sub-elements.
     * @param widthlimit Maximal width available for the element
     * @param force Use the area even if the used width is greater than maxwidth
     * @param linestart Indicates whether the element is placed at the line start
     * @return true if the box has been succesfully placed
     */
	@Override
    public boolean doLayout(int widthlimit, boolean force, boolean linestart)
    {
        //Skip if not displayed
        if (!displayed)
        {
            bounds.setSize(0, 0);
            return true;
        }
        
        setAvailableWidth(widthlimit);
        
        boolean split = false; //should we split to more boxes?
        boolean allow = false; //allow succesfull result even if nothing has been placed (line break only)
        boolean fail = false; //failed totally (nothing fit)
        int wlimit = getAvailableContentWidth();
        boolean empty = isempty;
        FontMetrics fm = g.getFontMetrics();
        int w = 0, h = 0;
        
        int end = textEnd;
        int lineend = text.indexOf('\r', textStart);
        if (lineend != -1 && lineend < end) //preserved end-of-line encountered
        {
        	end = lineend; //split at line end (or earlier)
        	split = true;
        	allow = true;
        }
        
        if (!empty || !linestart) //ignore empty text elements at the begining of a line
        {
            //ignore spaces at the begining of a line
            if ((linestart || ignoreinitialws) && collapsews)
                while (textStart < end && isWhitespace(text.charAt(textStart)))
                    textStart++;
            //try to place the text
            do
            {
                w = fm.stringWidth(text.substring(textStart, end));
                h = fm.getHeight();
                if (w > wlimit) //exceeded - try to split if allowed
                {
                    if (empty) //empty or just spaces - don't place at all
                    {
                        w = 0; h = 0; split = false; break;
                    }
                    int wordend = text.substring(0, end).lastIndexOf(' '); //find previous word
                    while (wordend > 0 && text.charAt(wordend-1) == ' ') wordend--; //skip trailing spaces
                    if (wordend <= textStart || !splitws) //no previous word, cannot split or splitting not allowed
                    {
                        if (!force) //everything failed
                        {
                        	//System.out.println("Here for " + this);
                            end = textEnd; //we will try with the whole rest next time
                            split = false; 
                            allow = false; //split before the linebreak
                            fail = true;
                        }
                        else
                            split = true;
                        break;
                    }
                    else
                    {
                        end = wordend;
                        split = true;
                    }
                }
            } while (end > textStart && w > wlimit);
        }
        textEnd = end;
        bounds.setSize(w, h);
        
        //if not the whole element was placed, create the rest
        if (split)
        {
            //skip the eventual line break
            int start = textEnd;
            if (start < text.length() && isLineBreak(text.charAt(start)))
            {
                start++;
                lineBreakStop = true;
            }
            //find the start of the next word
            if (collapsews)
            {
                while (start < text.length() && isWhitespace(text.charAt(start)))
                    start++;
            }
            //create the rest if something has left
            if (start < text.length())
            {
                TextBox rtext = copyTextBox();
                rtext.splitted = true;
                rtext.splitid = splitid + 1;
                rtext.setTextStart(start);
                rtext.setTextEnd(text.length());
                rest = rtext;
            }
            else
                rest = null;
        }
        else
            rest = null;
        
        return !fail && ((textEnd > textStart) || empty || allow);
    }
    
	@Override
    public void absolutePositions()
    {
	    updateStackingContexts();
        if (displayed)
        {
            //my top left corner
            absbounds.x = getParent().getAbsoluteContentX() + bounds.x;
            absbounds.y = getParent().getAbsoluteContentY() + bounds.y;
	        absbounds.width = bounds.width;
	        absbounds.height = bounds.height;
        }
    }
    
	@Override
    public int getMinimalWidth()
    {
		return minwidth;
    }
	
    private int computeMinimalWidth()
    {
        int ret = 0;
        String t = getText();
        if (t.length() > 0)
        {
            if (splitws)
            {
                //wrapping allowed - returns the length of the longest word
                ret = getLongestWord();
            }
            else
            {
            	//cannot wrap - return the width of the whole string
                ret = longestLineLength;
            }
        }
        return ret;
    }
    
	@Override
    public int getMaximalWidth()
    {
		return maxwidth;
    }
	
    private int computeMaximalWidth()
    {
        if (linews)
        {
            //no preserved line breaks -- returns the lenth of the whole string
            int len = g.getFontMetrics().stringWidth(getText());
            firstLineLength = len;
            lastLineLength = len;
            longestLineLength = len;
            return len;
        }
        else
        {
            return longestLineLength;
        }
    }
    
    private int getLongestWord()
    {
        int ret = 0;
        String t = getText();
        FontMetrics fm = g.getFontMetrics();
        
        int s1 = 0;
        int s2 = t.indexOf(' ');
        do
        {
            if (s2 == -1) s2 = t.length();
            int w = fm.stringWidth(t.substring(s1, s2));
            if (w > ret) ret = w;
            s1 = s2 + 1;
            s2 = t.indexOf(' ', s1);
        } while (s1 < t.length() && s2 < t.length());
        
        return ret;
    }

    public int getFirstLineLength()
    {
        return firstLineLength;
    }

    public int getLastLineLength()
    {
        return lastLineLength;
    }

    public boolean containsLineBreak()
    {
        return containsLineBreak;
    }
    
    public boolean finishedByLineBreak()
    {
        return lineBreakStop;
    }

    /**
     * Computes the lengths of the first, last and longest lines.
     */
    protected void computeLineLengths()
    {
        firstLineLength = -1;
        lastLineLength = 0;
        longestLineLength = 0;
        
        String t = getText();
        FontMetrics fm = g.getFontMetrics();
        
        int s1 = 0;
        int s2 = t.indexOf('\r');
        int w = 0;
        do
        {
            if (s2 == -1)
                s2 = t.length();
            else
                containsLineBreak = true;
            w = fm.stringWidth(t.substring(s1, s2));
            if (firstLineLength == -1) firstLineLength = w;
            if (w > longestLineLength) longestLineLength = w;
            s1 = s2 + 1;
            s2 = t.indexOf('\r', s1);
        } while (s1 < t.length() && s2 < t.length());
        lastLineLength = w;
    }

    /** 
     * Draw the text content of this box (no subboxes)
     * @param g the graphics context to draw on
     */
    public void drawContent(Graphics2D g)
    {
        //top left corner
        int x = absbounds.x;
        int y = absbounds.y;

        //Draw the string
        if (textEnd > textStart)
        {
            String t = text.substring(textStart, textEnd);
            Shape oldclip = g.getClip();
            if (clipblock != null)
                g.setClip(applyClip(oldclip, clipblock.getClippedContentBounds()));
            ctx.updateGraphics(g);
            
            if (!ctx.getTextDecoration().isEmpty()) 
            {
                AttributedString as = new AttributedString(t);
                as.addAttribute(TextAttribute.FONT, ctx.getFont());
                if (ctx.getTextDecoration().contains(CSSProperty.TextDecoration.UNDERLINE))
                    as.addAttribute(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON);
                if (ctx.getTextDecoration().contains(CSSProperty.TextDecoration.LINE_THROUGH))
                    as.addAttribute(TextAttribute.STRIKETHROUGH, TextAttribute.STRIKETHROUGH_ON);
                g.drawString(as.getIterator(), x, y + getBaselineOffset());
            } 
            else
                g.drawString(t, x, y + getBaselineOffset());
            
            g.setClip(oldclip);
        }
    }
    
	@Override
    public void draw(DrawStage turn)
    {
        if (displayed && isVisible())
        {
            if (turn == DrawStage.DRAW_INLINE)
            {
                getViewport().getRenderer().renderTextContent(this);
            }
        }
    }
    
	@Override
    public void drawExtent(Graphics2D g)
    {
        //draw the full box
        g.setColor(Color.ORANGE);
        g.drawRect(absbounds.x, absbounds.y, bounds.width, bounds.height);
        /*for (int i = 0; i < getText().length(); i++)
        {
            if (i != 0) System.out.print(" : ");
            char ch = getText().charAt(i);
            System.out.print(ch);
            System.out.print((int) ch);
            if (Character.isWhitespace(ch))
                System.out.print("!");
            if (Character.isSpaceChar(ch))
                System.out.print("*");
        }
        System.out.println();*/
        
        g.setColor(Color.MAGENTA);
        int y = getAbsoluteContentY();
        int h = getTotalLineHeight();
            
        g.drawRect(getAbsoluteContentX(), y, getContentWidth(), h);
        
        g.setColor(Color.BLUE);
        y = getAbsoluteContentY() + getBaselineOffset();
        g.drawRect(getAbsoluteContentX(), y, getContentWidth(), 1);
        
        
    }
	
	//===============================================================================
	
	/**
	 * Checks if a character can be interpreted as whitespace according to current settings.
	 * @param ch the character
	 * @return true when ch is a whitespace character
	 */
    private boolean isWhitespace(char ch)
    {
        if (linews) 
            return Character.isWhitespace(ch);
        else
            return ch != '\n' && ch != '\r' && Character.isWhitespace(ch);
    }
    
    /**
     * Checks if a character can be interpreted as a line break according to current settings.
     * @param ch the character
     * @return true when ch is the line break
     */
    private boolean isLineBreak(char ch)
    {
        if (linews)
            return false;
        else
            return (ch == '\r' || ch == '\n');
    }
    

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy