TextWrapper.java

/**
 * Copyright (c) 2012-2013 Nokia Corporation. All rights reserved.
 * Nokia and Nokia Connecting People are registered trademarks of Nokia Corporation.
 * Oracle and Java are trademarks or registered trademarks of Oracle and/or its
 * affiliates. Other product and company names mentioned herein may be trademarks
 * or trade names of their respective owners.
 * See LICENSE.TXT for license information.
 */

package com.nokia.example.amaze.ui;

import javax.microedition.lcdui.Font;

/**
 * Utility class for text word wrapping with Graphics.drawString().
 */
public class TextWrapper {
    // Constants
    private char LINE_BREAK = '\n';
    private char WHITE_SPACE = ' ';
    
    // Members
    static private TextWrapper _instance = null;
    private String _text = null;
    private Font _font;
    private int _fontHeight;
    private int _totalLength = 0;
    private int _width = 0;
    private int _start = 0;
    private int _position = 0;
    
    /**
     * Getters for the singleton instance.
     * @param width The width of the text area.
     */
    static public TextWrapper instance(final Font font, final int width) {
        if (_instance == null) {
            _instance = new TextWrapper(font, width);
        }
        else {
            _instance.setFont(font);
            _instance.setWidth(width);
        }
        
        return _instance;
    }
    
    /**
     * Constructor.
     * @param width The width of the text area.
     */
    private TextWrapper(Font font, int width) {
        if (font == null || width <= 0) {
            throw new IllegalArgumentException();
        }
        
        setFont(font);
        setWidth(width);
    }
    
    /**
     * Sets the font. Used for determine the text size.
     * @param font The font to set.
     */
    public void setFont(final Font font) {
        if (font != null && _font != font) {
            _font = font;
            _fontHeight = _font.getHeight();
        }
    }
    
    /**
     * Sets the width of the text area so that this wrapper knows where to cut
     * the line.
     * @param width The width of the text area.
     */
    public void setWidth(final int width) {
        if (width > 0) {
            _width = width;
        }
    }
    
    /**
     * @param text The text to wrap.
     */
    public void setText(String text) {
        if (text == null) {
            throw new IllegalArgumentException();
        }
        
        _text = text;
        _totalLength = _text.length();
        _start = 0;
        _position = 0;
    }
    
    /**
     * @return The text set currently.
     */
    public String text() {
        return _text;
    }
    
    /** 
     * @return The length of the given line.
     */
    public final int lineWidth(final String line) {
        return _font.stringWidth(line);
    }

    /** 
     * @return The height of a single text line.
     */
    public final int lineHeight() {
        return _fontHeight;
    }

    /** 
     * @return The number of lines the current text will be divided to.
     */
    public int lineCount() {
        if (_text == null && _totalLength == 0) {
            return 0;
        }
        
        String original = _text;
        int count = 0;
        
        while (hasMoreLines()) {
            nextLine();
            count++;
        }
        
        setText(original); // Reset
        return count;
    }
    
    /** 
     * @return True if there are still one or more lines left to paint, false
     *            otherwise.
     */
    public boolean hasMoreLines() {
        return (_position < (_totalLength - 1));
    }
    
    /** 
     * @return The next line to paint.
     */
    public String nextLine() {
        int next = nextPosition();

        if (_start >= _totalLength || next > _totalLength) {
            return null;
        }
        
        String retval = _text.substring(_start, next);
        _start = next;
        
        if ((_totalLength - 1 > _start)
                && ((_text.charAt(_start) == LINE_BREAK)
                || (_text.charAt(_start) == WHITE_SPACE)))
        {
            _position++;
            _start++;
        }

        return retval.trim();
    }

    /** 
     * @return The next position where to cut the string (i.e. end position of
     *            the next line).
     */
    private int nextPosition() {
        int i = nextWordIndex(_position);
        int lastBreak = -1;
        String line = _text.substring(_position, i);
        int lineWidth = _font.stringWidth(line);
 
        while (i < _totalLength && lineWidth <= _width) {
            if (_text.charAt(i) == WHITE_SPACE) {
                lastBreak = i;
            } 
            else if (_text.charAt(i) == LINE_BREAK) {
                lastBreak = i;
                break;
            }

            if (++i < _totalLength) {
                i = nextWordIndex(i);
                line = _text.substring(_position, i);
                lineWidth = _font.stringWidth(line);
            }
        }

        if (i == _totalLength && lineWidth <= _width) {
            _position = i;
        }
        else if (lastBreak == _position) {
            _position++;
        }
        else if (lastBreak < _position) {
            _position = i;
        }
        else {
            _position = lastBreak;
        }

        return _position;
    }
    
    /**
     * Resolves the position (index) of the next word from the given
     * position (startIndex). 
     * @param startIndex The position where to start looking for the next
     *                      starting word.
     * @return The index of the next starting word.
     */
    private int nextWordIndex(int startIndex) {
        int space = _text.indexOf(WHITE_SPACE, startIndex);
        int newLine = _text.indexOf(LINE_BREAK, startIndex);

        if (space == -1) { 
            space = _totalLength;
        }
        
        if (newLine == -1) {
            newLine = _totalLength;
        }

        if (space < newLine) {
            return space;
        }
        
        return newLine;
    }
}