Package games.stendhal.client.gui.j2d

Source Code of games.stendhal.client.gui.j2d.TextBoxFactory

/* $Id: TextBoxFactory.java,v 1.15 2011/06/29 09:17:11 kiheru Exp $ */
/***************************************************************************
*                   (C) Copyright 2003-2010 - Stendhal                    *
***************************************************************************
***************************************************************************
*                                                                         *
*   This program is free software; you can redistribute it and/or modify  *
*   it under the terms of the GNU General Public License as published by  *
*   the Free Software Foundation; either version 2 of the License, or     *
*   (at your option) any later version.                                   *
*                                                                         *
***************************************************************************/
package games.stendhal.client.gui.j2d;

import games.stendhal.client.FormatTextParserExtension;
import games.stendhal.client.gui.FormatTextParser;
import games.stendhal.client.sprite.ImageSprite;
import games.stendhal.client.sprite.Sprite;

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsEnvironment;
import java.awt.Polygon;
import java.awt.RenderingHints;
import java.awt.Transparency;
import java.awt.font.TextAttribute;
import java.awt.image.BufferedImage;
import java.text.AttributedCharacterIterator;
import java.text.AttributedString;
import java.text.BreakIterator;
import java.text.CharacterIterator;
import java.util.LinkedList;
import java.util.List;

import org.apache.log4j.Logger;

/**
* A helper class for painting speech bubbles and other
* messages used on the screen.
*/
public class TextBoxFactory {
  /** Used for calculating the line metrics */
  private static final Graphics graphics = (new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB)).getGraphics();
 
  /** space to be left at the beginning and end of line in pixels. */
  private static final int MARGIN_WIDTH = 3;
  /** height of text lines in pixels. */
  private static final int LINE_HEIGHT = 16;
  /** space needed for the bubble "handle" in pixels. */
  private static final int BUBBLE_OFFSET = 10;
  /** the diameter of the arc of the rounded bubble corners. */
  private static final int ARC_DIAMETER = 2 * MARGIN_WIDTH + 2;
  /**
   * The maximum number of lines to try to fit in a text box. It is not a
   * hard limit, but can be exceeded by one in some situations.
   */
  private static final int MAX_LINES = 6;
 
  /**
   * Creates a text box sprite.
   *
   * @param text the text inside the box
   * @param width maximum width of the text in the box in pixels
   * @param textColor color of the text
   * @param fillColor background color
   * @param isTalking true if the box should look like a chat bubble
   *
   * @return sprite of the text box
   */
  public Sprite createTextBox(final String text, final int width, final Color textColor,
      final Color fillColor, final boolean isTalking) {

    // Format before splitting to get the coloring right
    final AttributedString formattedString = formatLine(text.trim(), graphics.getFont(), textColor);
    // split it to max width long pieces
    final List<AttributedCharacterIterator> formattedLines = splitFormatted(formattedString, width);
   
    // Find the actual width of the text
    final int lineLengthPixels = getMaxPixelWidth(formattedLines);
    final int numLines = formattedLines.size();

    final GraphicsConfiguration gc = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration();
    final int imageWidth;
    if (lineLengthPixels + BUBBLE_OFFSET < width) {
      imageWidth = (lineLengthPixels + BUBBLE_OFFSET) + ARC_DIAMETER;
    } else {
      imageWidth = width + BUBBLE_OFFSET + ARC_DIAMETER;
    }
   
    final int imageHeight = LINE_HEIGHT * numLines + MARGIN_WIDTH;

    final BufferedImage image = gc.createCompatibleImage(imageWidth, imageHeight, Transparency.BITMASK);

    final Graphics2D g2d = image.createGraphics();
    g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

    if (fillColor != null) {
      if (isTalking) {
        drawBubble(g2d, fillColor, textColor, imageWidth - BUBBLE_OFFSET, imageHeight);
      } else {
        drawRectangle(g2d, fillColor, textColor, imageWidth - BUBBLE_OFFSET, imageHeight);
      }
    }

    drawTextLines(g2d, formattedLines, textColor);

    return new ImageSprite(image);
  }
 
   /**
    * Draw a chat bubble. 
    *
    * @param g2d
    * @param fillColor the bacground color of the bubble
    * @param outLineColor the color of the bubble outline
    * @param width width of the bubble body
    * @param height height of the bubble
    */
  private void drawBubble(final Graphics2D g2d, final Color fillColor,
      final Color outLineColor, final int width, final int height) {
    /*
     * There's an one pixel difference in how sun java and openjdk
     * do drawRoundRect, so we use fillRoundRect for both the
     * outline and the fill to have pretty bubbles on both
     */
    g2d.setColor(outLineColor);
    g2d.fillRoundRect(BUBBLE_OFFSET, 0, width, height, ARC_DIAMETER, ARC_DIAMETER);
    g2d.setColor(fillColor);
    g2d.fillRoundRect(BUBBLE_OFFSET + 1, 1, width - 2, height - 2, ARC_DIAMETER, ARC_DIAMETER);
   
    // The bubble handle
    final Polygon p = new Polygon();
    p.addPoint(BUBBLE_OFFSET + 1, MARGIN_WIDTH + 1);
    p.addPoint(0, LINE_HEIGHT);
    p.addPoint(BUBBLE_OFFSET + 1, LINE_HEIGHT / 2 + MARGIN_WIDTH);
    g2d.fillPolygon(p);
   
    g2d.setColor(outLineColor);
    g2d.drawLine(0, LINE_HEIGHT, BUBBLE_OFFSET, MARGIN_WIDTH + 1);
    g2d.drawLine(0, LINE_HEIGHT, BUBBLE_OFFSET, LINE_HEIGHT / 2 + MARGIN_WIDTH);
  }
 
  /**
   * Draw an outlined rectangle.
   *
   * @param g2d graphics
   * @param fillColor The background color of the rectangle
   * @param outLineColor Color of the outline
   * @param width Pixel width of the drawn rectangle
   * @param height Pixel height of the drawn rectangle
   */
  private void drawRectangle(final Graphics2D g2d, final Color fillColor,
      final Color outLineColor, final int width, final int height) {
    // Using filled rectangles to work around a rendering
    // incompatibility in openjdk.
    g2d.setColor(outLineColor);
    g2d.fillRect(BUBBLE_OFFSET, 0, width, height);
    g2d.setColor(fillColor);
    g2d.fillRect(BUBBLE_OFFSET + 1, 1, width - 2, height -2);
  }

  /**
   * Color a string according to the formatting characters in it.
   *
   * @param line string to be formatted
   * @param normalFont base font used for the text
   * @param normalColor base color used for the text
   * @return colored sting
   */
  private AttributedString formatLine(final String line,
        final Font normalFont, final Color normalColor) {
    final Font specialFont = normalFont.deriveFont(Font.ITALIC);

    try {
      // recreate the string without the # characters
      final StringBuilder temp = new StringBuilder();
      FormatTextParser parser = new FormatTextParserExtension(temp);
      parser.format(line);

      // create the attribute string including formating
      final AttributedString aStyledText = new AttributedString(temp.toString());

      parser = new FormatTextParser() {
        private int s = 0;

        @Override
        public void normalText(final String tok) {
          if (tok.length() > 0) {
            aStyledText.addAttribute(TextAttribute.FONT, normalFont, s, s
                + tok.length());
            aStyledText.addAttribute(TextAttribute.FOREGROUND, normalColor, s, s
                + tok.length());
            s += tok.length();
          }
        }

        @Override
        public void colorText(final String tok) {
          if (tok.length() > 0) {
            aStyledText.addAttribute(TextAttribute.FONT, specialFont, s, s
                + tok.length());
            aStyledText.addAttribute(TextAttribute.FOREGROUND, Color.blue, s, s
                + tok.length());
            s += tok.length();
          }
        }
      };
      parser.format(line);

      return aStyledText;
    } catch (final Exception e) {
      Logger.getLogger(TextBoxFactory.class).error(e, e);
      return null;
    }
  }
 
  /**
   * Splits a text to lines with specified maximum width, preserving the line breaks in the original.
   *
   * @param text the text to be split
   * @param width maximum line length in pixels
   *
   * @return list of lines
   */
  private List<AttributedCharacterIterator> splitFormatted(final AttributedString text, final int width) {
    final List<AttributedCharacterIterator> lines = new LinkedList<AttributedCharacterIterator>();

    final BreakIterator iter = BreakIterator.getLineInstance();
    iter.setText(text.getIterator());
   
    int previous = iter.first();
   
    AttributedCharacterIterator best = null;
   
    while (iter.next() != BreakIterator.DONE) {
      final AttributedCharacterIterator candidate = text.getIterator(null, previous, iter.current());

      if (getPixelWidth(candidate) <= width) {
        // check for line breaks within the provided text
        // unfortunately, the BreakIterators are too dumb to tell *why* they consider the
        // location a break, so the check needs to be implemented here
        final CharacterIterator cit = iter.getText();
        if (isHardLineBreak(cit)) {
          lines.add(candidate);
          previous = iter.current();
          best = null;
        } else {
          best = candidate;
        }
      } else {
        if (best == null) {
          // could not break the line - the word's simply too long. Use more force to
          // to fit it to the width
          best = splitAggressively(candidate, width);
          // splitAggressively returns an iterator with its own indexing,
          // so instead of using it directly we need to adjust the old one
          previous += best.getEndIndex() - best.getBeginIndex();
        } else {
          previous = best.getEndIndex();
          // Trim the trailing white space
          char endChar = best.last();

          int endIndex = previous;
          while (Character.isWhitespace(endChar) && endChar != CharacterIterator.DONE) {
            endIndex = best.getIndex();
            endChar = best.previous();
          }

          best = text.getIterator(null, best.getBeginIndex(), endIndex);
        }

        lines.add(best);

        // a special check for a hard line break just after the word
        // that got moved to the next line
        final CharacterIterator cit = iter.getText();
        if (isHardLineBreak(cit)) {
          lines.add(text.getIterator(null, previous, iter.current()));
          previous = iter.current();
        }
       
        // Pick the shortest candidate possible (backtrack a bit, if needed)
        if (iter.current() > previous + 1) {
          iter.previous();
        }

        best = null;

        if (lines.size() > MAX_LINES) {
          /*
           * Limit the height of the text boxes. Append ellipsis
           * to tell the user to take a look at the chat log.
           * The last line is removed twice to avoid the situation
           * where the last text line would fit on the space the
           * ellipsis occupies.
           */
          lines.remove(lines.size() - 1);
          lines.remove(lines.size() - 1);
          lines.add(new AttributedString("...").getIterator());
          return lines;
        }
      }
    }

    // add the rest of the text, if there's any
    if (previous < iter.last()) {
      lines.add(text.getIterator(null, previous, iter.last()));
    }

    return lines;
  }
 
  /**
   * Try splitting a line considering anything that looks like a word break a
   * valid line break point.
   * (should we break just anywhere if even that fails? now we just return
   * the whole line)
   * 
   * @param text iterator to the line that should be split
   * @param width the maximum allowed pixel width
   *
   * @return iterator to the part of the line that fits in width
   */
  private AttributedCharacterIterator splitAggressively(final AttributedCharacterIterator text, final int width) {
    final int offset = text.getBeginIndex();
    final BreakIterator wordIterator = BreakIterator.getWordInstance();
   
    final AttributedString tmpText = new AttributedString(text);
    // return the original iterator if there are no suitable break points
    AttributedCharacterIterator best = text;
    wordIterator.setText(text);
   
    while (wordIterator.next() != BreakIterator.DONE) {
      final AttributedCharacterIterator candidate = tmpText.getIterator(null, tmpText.getIterator().getBeginIndex(), wordIterator.current() - offset);
     
      if (getPixelWidth(candidate) <= width) {
        best = candidate;
      } else {
        return best;
      }
    }
   
    // should never be reached, but java is trying to be too smart and does
    // not allow throwing exceptions here
    return best;
  }

  /**
   * Get the longest pixel width of a list of lines.
   *
   * @param lines lines to be checked
   * @return the longest pixel width of the checked lines
   */
  private int getMaxPixelWidth(final List<AttributedCharacterIterator> lines) {
    int pixelWidth = 0;

    for (final AttributedCharacterIterator line : lines) {
      final int width = getPixelWidth(line);
      if (width > pixelWidth) {
        pixelWidth = width;
      }
    }
   
    return pixelWidth;
  }
 
  /**
   * Get the pixel width of a text line.
   *
   * @param iter iterator representing the text line
   * @return pixel width of the line
   */
  private int getPixelWidth(final AttributedCharacterIterator iter) {
    return (int) graphics.getFontMetrics().getStringBounds(iter, iter.getBeginIndex(), iter.getEndIndex(), graphics).getWidth();
  }

  /**
   * Draw a list of text lines.
   *
   * @param g2d graphics where the text should be drawn
   * @param lines the text lines to be drawn
   * @param textColor the base color of the text
   */
  private void drawTextLines(final Graphics2D g2d, final List<AttributedCharacterIterator> lines, final Color textColor) {
    int i = 0;
    for (final AttributedCharacterIterator line : lines) {
      if (textColor == null) {
        g2d.setColor(Color.black);
      }
      g2d.setColor(textColor);

      g2d.drawString(line, BUBBLE_OFFSET + MARGIN_WIDTH, MARGIN_WIDTH + i * LINE_HEIGHT + 10);
      i++;
    }
  }
 
  /**
   * Check if a location is at a hard line break.
   *
   * @param cit
   * @return <code>true</code> if there is a hard line break
   */
  private boolean isHardLineBreak(final CharacterIterator cit) {
    // save the location while we are checking the preceding characters
    final int currentIndex = cit.getIndex();
   
    char currentChar = cit.previous();
    while (currentChar != CharacterIterator.DONE && !Character.isLetterOrDigit(currentChar)) {
      if (currentChar == '\n') {
        cit.setIndex(currentIndex);
        return true;
      }
      currentChar = cit.previous();
    }
    cit.setIndex(currentIndex);
   
    return false;
  }
}
TOP

Related Classes of games.stendhal.client.gui.j2d.TextBoxFactory

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.