/*
* Copyright 2009 Rodrigo Reyes reyes.rr at gmail dot com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package net.kornr.swit.button;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.font.FontRenderContext;
import java.awt.font.LineMetrics;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.LinkedList;
import java.util.List;
import net.kornr.swit.button.effect.Effect;
/**
* The ButtonTemplate is the basis for all the buttons generator.
* <p>
* Classes extending ButtonTemplate shall implement the following methods:
* <ul>
* <li> drawBackground(BufferedImage img, String text): draw the background of the button (or do nothing if there's nothing to draw in the background) </li>
* <li> drawForeground(BufferedImage img, String text): draw the foreground of the button (or do nothing if there's no foreground for this button)</li>
* </ul>
*
*/
abstract public class ButtonTemplate
{
final static public int TEXT_TRANSFORM_NONE = 1;
final static public int TEXT_TRANSFORM_ALLCAPS = 2;
final static public int TEXT_ALIGN_LEFT = 1;
final static public int TEXT_ALIGN_CENTER = 2;
final static public int TEXT_ALIGN_RIGHT = 3;
private int m_textTransform = TEXT_TRANSFORM_NONE;
private int m_textAlign = TEXT_ALIGN_CENTER;
private int m_width = 150;
private int m_height = 35;
private float m_smallCapsRatio = 0.75f;
private boolean m_autoExtend = true;
private boolean m_shadowDisplayed = false;
private Font m_font = null;
private Color m_fontColor = Color.white;
private List<Effect> m_effects = new LinkedList<Effect>();
/**
* This is the main method that clients should use to create an image with the corresponding text.
*
* @param text the text to generate with this button template
* @return an image of the button
*/
public BufferedImage getImage(String text)
{
int width = m_width;
int height = m_height;
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = image.createGraphics();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
Font font = getTextFont(g);
g.setFont(font);
FontRenderContext frc = g.getFontRenderContext();
List<TextElement> elements = buildTextElement(text, font);
Rectangle2D bounds = getBounds(frc, elements);
TextPadding padding = null;
if (isAutoExtend())
{
int extendedwidth = (int) bounds.getWidth();
int extendedheight = (int) bounds.getHeight();
padding = this.getTextPadding(extendedwidth, extendedheight, elements);
extendedwidth += padding.getLeft()+padding.getRight();
extendedheight += padding.getTop()+padding.getBottom();
if (m_shadowDisplayed)
{
extendedwidth++;
extendedheight++;
}
int newwidth = Math.max(extendedwidth, width);
int newheight = Math.max(extendedheight, height);
if (width != newwidth || height != newheight)
image = new BufferedImage(newwidth, newheight, BufferedImage.TYPE_INT_ARGB);
}
else
{
padding = this.getTextPadding(width, height, elements);
}
BufferedImage replacementImage = null;
replacementImage = this.drawBackground(image, text);
if (replacementImage != null)
image = replacementImage;
Rectangle2D.Double textBound = new Rectangle2D.Double(padding.getLeft(), padding.getTop(), image.getWidth()-padding.getLeft()-padding.getRight(), image.getHeight()-padding.getTop()-padding.getBottom());
this.drawText(image, textBound, elements, bounds, g.getFontMetrics(font).getAscent());
replacementImage = this.drawForeground(image, text);
if (replacementImage != null)
image = replacementImage;
for (Effect e: m_effects)
{
image = e.apply(image);
}
return image;
}
/**
* Draw the background of the image.
* @param img The bufferedImage in which to draw the background
* @param text the text of the button. This is for information only, as the ButtonTemplate is responsible for drawing the text.
* @return null if the changed occurred in the img Image given as parameter, or a new image buffer.
*/
abstract protected BufferedImage drawBackground(BufferedImage img, String text);
/**
* Draw the foreground of the image.
* @param img The bufferedImage in which to draw the foreground
* @param text the text of the button. This is for information only, as the text is already drawn at the time this method is called.
* @return null if the changed occurred in the img Image given as parameter, or a new image buffer.
*/
abstract protected BufferedImage drawForeground(BufferedImage img, String text);
/**
* Defines the padding (spaces around the text) to apply on this button. This is required when the buttons need a specific margin to be applied
* around the text to avoid overlapping with graphical elements of the background or foreground.
*
* @param width the width of the image containing the text
* @param height
* @param elements
* @return
*/
protected TextPadding getTextPadding(int width, int height, List<TextElement> elements)
{
return new TextPadding(5,5,5,5);
}
protected class TextElement
{
public Font font;
public String text;
public Color color;
private double width;
public TextElement(Font f, String t)
{
this(f,t,m_fontColor);
}
public TextElement(Font f, String t, Color col)
{
this.font = f;
this.text = t;
this.color = col;
}
}
protected Rectangle2D drawText(BufferedImage image, Rectangle2D target, List<TextElement> elements, Rectangle2D textBounds, float fontAscent)
{
Rectangle2D.Float result = new Rectangle2D.Float();
Graphics2D g = image.createGraphics();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g.setColor(m_fontColor);
float posx, posy;
switch(m_textAlign)
{
case TEXT_ALIGN_LEFT:
posx = (float)target.getX();
break;
case TEXT_ALIGN_RIGHT:
posx = (float) (target.getX()+ target.getWidth() - textBounds.getWidth() );
break;
default:
posx = (float)target.getX() + (float)(target.getWidth()-textBounds.getWidth())/2f; // center
break;
}
posy = (float)target.getY() + (float)(target.getHeight()-textBounds.getHeight())/2f; // center
// posy += textBounds.getY();
posy += fontAscent;
if (m_shadowDisplayed)
{
this.drawShadow(g, elements, posx+1, posy+1);
}
this.draw(g, elements, posx, posy);
result.setRect(posx, posy-fontAscent, textBounds.getWidth(), textBounds.getHeight());
return result;
}
private void draw(Graphics2D g, List<TextElement> elements, float posx, float posy)
{
for (TextElement el : elements)
{
g.setFont(el.font);
g.setColor(el.color);
g.drawString(el.text, posx, posy);
posx += el.width;
}
}
private void drawShadow(Graphics2D g, List<TextElement> elements, float posx, float posy)
{
for (TextElement el : elements)
{
g.setFont(el.font);
g.setColor(Color.black);
g.drawString(el.text, posx, posy);
posx += el.width;
}
}
private Rectangle2D getBounds(FontRenderContext frc, List<TextElement> elements)
{
double width = 0;
double maxascent = 0;
double maxheight = 0;
for (TextElement el : elements)
{
Rectangle2D bounds = el.font.getStringBounds(el.text, frc);
el.width = bounds.getWidth();
width += el.width;
LineMetrics metrics = el.font.getLineMetrics(el.text, frc);
maxascent = Math.max(maxascent, metrics.getAscent());
maxheight = Math.max(maxheight, metrics.getAscent()+metrics.getDescent()+metrics.getLeading());
}
return new Rectangle2D.Double(0,maxascent,width,Math.ceil(maxheight));
}
/**
* Create a list of TextElement objects that represent the text to display.
* This methods manages the parsing and creation of TextElement.
*
* @param text a String object that represents the text of the button
* @param defaultFont the default font to use, if no font is specified in the text
* @return alist of TextElement object. If the string is empty, returns an empty list.
*/
protected List<TextElement> buildTextElement(String text, Font defaultFont)
{
List<TextElement> els = splitWords(text, defaultFont);
switch(m_textTransform)
{
case ButtonTemplate.TEXT_TRANSFORM_ALLCAPS:
return makeAllCaps(els);
default:
return els;
}
}
private List<TextElement> splitWords(String text, Font defaultFont)
{
LinkedList<TextElement> res = new LinkedList<TextElement>();
String[] split = text.split(" ");
boolean firstword = true;
Font font = m_font;
if (m_font == null)
font = defaultFont;
for (String s:split)
{
if (!firstword)
res.add(new TextElement(font, " "));
else
firstword = false;
if (s.trim().length()>0)
res.add(new TextElement(font, s));
}
return res;
}
private List<TextElement> makeAllCaps(List<TextElement> elements)
{
LinkedList<TextElement> res = new LinkedList<TextElement>();
for (TextElement el : elements)
{
String s = el.text;
if (s.length()>1)
{
TextElement e1 = new TextElement(el.font, el.text.substring(0, 1).toUpperCase());
res.add(e1);
float fs = el.font.getSize();
fs *= m_smallCapsRatio;
Font smaller = el.font.deriveFont(fs);
TextElement e2 = new TextElement(smaller, el.text.substring(1).toUpperCase());
res.add(e2);
}
else
{
el.text = el.text.toUpperCase();
res.add(el);
}
}
return res;
}
public Font getFont() {
return m_font;
}
public void setFont(Font font) {
m_font = font;
}
public int getWidth() {
return m_width;
}
public void setWidth(int width) {
m_width = width;
}
public int getHeight() {
return m_height;
}
public void setHeight(int height) {
m_height = height;
}
public boolean isAutoExtend() {
return m_autoExtend;
}
public void setAutoExtend(boolean autoExtend) {
m_autoExtend = autoExtend;
}
public Color getFontColor() {
return m_fontColor;
}
public void setFontColor(Color fontColor) {
m_fontColor = fontColor;
}
public int getTextTransform() {
return m_textTransform;
}
public void setTextTransform(int textTransform) {
m_textTransform = textTransform;
}
public float getSmallCapsRatio() {
return m_smallCapsRatio;
}
public void setSmallCapsRatio(float smallCapsRatio) {
m_smallCapsRatio = smallCapsRatio;
}
public boolean isShadowDisplayed() {
return m_shadowDisplayed;
}
public void setShadowDisplayed(boolean shadowDisplayed) {
m_shadowDisplayed = shadowDisplayed;
}
public int getTextAlign() {
return m_textAlign;
}
public void setTextAlign(int textAlign) {
m_textAlign = textAlign;
}
protected Graphics2D initializeGraphics2D(BufferedImage img)
{
Graphics2D g = img.createGraphics();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
return g;
}
/**
* Add an effect to apply on the button. Effects are applied in the same order they are added.
* @param effect
* @return
*/
public ButtonTemplate addEffect(Effect effect)
{
m_effects.add(effect);
return this;
}
public List<Effect> getEffects() {
return m_effects;
}
public void setEffects(List<Effect> effects) {
m_effects = effects;
}
protected Font getTextFont(Graphics2D g)
{
Font font = m_font;
if (font == null)
font = g.getFont();
return font;
}
}