TextWrapper.java

/*
 * Copyright © 2012 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;
	}
}