/**
* Copyright (c) 2003-2009, Xith3D Project Group all rights reserved.
*
* Portions based on the Java3D interface, Copyright by Sun Microsystems.
* Many thanks to the developers of Java3D and Sun Microsystems for their
* innovation and design.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of the 'Xith3D Project Group' nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) A
* RISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE
*/
package org.xith3d.ui.hud.widgets;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import org.jagatoo.input.devices.components.DeviceComponent;
import org.jagatoo.input.devices.components.Key;
import org.jagatoo.input.devices.components.Keys;
import org.openmali.types.twodee.Dim2f;
import org.openmali.types.twodee.Dim2i;
import org.openmali.vecmath2.Colorf;
import org.xith3d.loop.UpdatingThread.TimingMode;
import org.xith3d.loop.opscheduler.Interval;
import org.xith3d.loop.opscheduler.OperationScheduler;
import org.xith3d.scenegraph.Texture2D;
import org.xith3d.scenegraph.Texture2DCanvas;
import org.xith3d.ui.hud.HUD;
import org.xith3d.ui.hud.base.Border;
import org.xith3d.ui.hud.listeners.TextFieldListener;
import org.xith3d.ui.hud.utils.Cursor;
import org.xith3d.ui.hud.utils.DrawUtils;
import org.xith3d.ui.hud.utils.HUDFont;
import org.xith3d.ui.hud.utils.HUDTextureUtils;
import org.xith3d.ui.hud.utils.TileMode;
import org.xith3d.ui.text2d.TextAlignment;
/**
* A TextField is a Widget that allows for editing a single line of text.
*
* @author Marvin Froehlich (aka Qudus)
*/
public class TextField extends Label
{
public static class Description extends Label.Description
{
private Border.Description borderDesc;
private Texture2D caretTexture;
public void setBorderDescription( Border.Description borderDesc )
{
this.borderDesc = borderDesc;
}
public final Border.Description getBorderDescription()
{
return ( borderDesc );
}
public void setCaretTexture( Texture2D texture )
{
this.caretTexture = texture;
}
public void setCaretTexture( String texture )
{
setCaretTexture( HUD.getTheme().getTextCaretTexture( texture ) );
}
public final Texture2D getCaretTexture()
{
return ( caretTexture );
}
public void set( Description template )
{
super.set( template );
this.borderDesc = template.borderDesc.clone();
this.caretTexture = template.caretTexture;
}
@Override
public Description clone()
{
return ( new Description( this ) );
}
protected Description( Description template )
{
super( template );
this.set( template );
}
public Description( Label.Description labelDesc, Border.Description borderDesc, Texture2D caretTexture )
{
super( labelDesc );
this.borderDesc = borderDesc;
this.caretTexture = caretTexture;
}
public Description( Label.Description labelDesc, Border.Description borderDesc, String caretTexture )
{
this( labelDesc, borderDesc, HUDTextureUtils.getTexture( caretTexture, true ) );
}
public Description( Label.Description labelDesc, Border.Description borderDesc )
{
this( labelDesc, borderDesc, (Texture2D)null );
}
}
private static class CaretBlinker extends Interval
{
private TextField currentTextField = null;
private final Image caretImage;
@Override
protected void onIntervalHit( long gameTime, long frameTime, TimingMode timingMode )
{
super.onIntervalHit( gameTime, frameTime, timingMode );
caretImage.setVisible( currentTextField.isEditable() && currentTextField.hasFocus( true ) && !caretImage.isVisible() );
currentTextField.setTextureDirty();
}
private void init()
{
/*
Dim2f buffer = Dim2f.fromPool();
currentTextField.getSizePixels2HUD_( HUDTextureUtils.getTextureWidth( currentTextField.caretTex ), 0, buffer );
float caretWidth = buffer.getWidth();
currentTextField.getSizePixels2HUD_( 1, 1, buffer );
float caretHeight = currentTextField.getContentHeight() - 2f * buffer.getHeight();
Dim2f.toPool( buffer );
caretImage.setSize( caretWidth, caretHeight );
caretImage.setTexture( currentTextField.caretTex );
currentTextField.getWidgetAssembler().addWidget( caretImage );
caretImage.setZIndex( currentTextField.getZIndex() + 1 );
*/
currentTextField.updateCaretPosition();
caretImage.setVisible( currentTextField.isEditable() && currentTextField.hasFocus( true ) );
currentTextField.setTextureDirty();
}
public void start( TextField textField, OperationScheduler opScheder )
{
if ( ( caretImage != null ) && ( caretImage.getContainer() != null ) )
{
caretImage.detach();
}
this.currentTextField = textField;
init();
if ( !this.isAlive() )
opScheder.addInterval( this );
}
public void stop()
{
super.kill();
if ( caretImage != null )
{
caretImage.setVisible( false );
caretImage.detach();
}
}
public CaretBlinker( long rate )
{
super( rate );
this.caretImage = new Image( 1f, 16f, HUD.getTheme().getTextCaretTexture( "black" ), TileMode.STRETCH );
caretImage.setVisible( false );
stop();
}
}
private String dispText = "";
private static final CaretBlinker blinker = new CaretBlinker( 500000L );
private final Texture2D caretTex;
private int caretPosByChars;
private float caretLeftHUD;
private boolean editable = true;
private char echoChar = '*';
private boolean echoMode = false;
private char[] ignoredChars = new char[] { '^' };
private final ArrayList< TextFieldListener > listeners = new ArrayList< TextFieldListener >();
/**
* Adds a listener, that is notified of special TextField events.
*
* @param l
*/
public void addTextFieldListener( TextFieldListener l )
{
listeners.add( l );
}
/**
* Removes a TextFieldListener from the list.
*
* @param l
*/
public void removeTextFieldListener( TextFieldListener l )
{
listeners.remove( l );
}
/**
* Enables or disables the editability of this TextField. If not editable,
* no key-events will be processed and the carret won't be visible.
*
* @param editable
*/
public void setEditable( boolean editable )
{
if ( editable == this.editable )
return;
this.editable = editable;
if ( editable && this.hasFocus( true ) )
{
blinker.start( this, getHUD().getOperationScheduler() );
}
}
/**
* @return the editability of this TextField. If not editable, no key-events
* will be processed and the carret won't be visible.
*/
public boolean isEditable()
{
return ( editable );
}
// TODO: This should be removed, since direct texture drawing already has a context!
private static Graphics2D graphics = new BufferedImage( 16, 16, BufferedImage.TYPE_3BYTE_BGR ).createGraphics();
private final float getCharWidth( char ch )
{
FontMetrics metrics = graphics.getFontMetrics( getFont( !isEnabled() ).getAWTFont( getHUD() ) );
Rectangle2D bounds = metrics.getStringBounds( String.valueOf( ch ), graphics );
return ( (float)bounds.getWidth() );
}
private final float getEchoCharWidth()
{
return ( getCharWidth( getEchoChar() ) );
}
private float getCharWidth( int i )
{
if ( i >= getDisplayedText().length() )
return ( 0 );
return ( getCharWidth( getDisplayedText().charAt( i ) ) );
}
private final float computeCaretPos()
{
FontMetrics metrics = graphics.getFontMetrics( getFont( !isEnabled() ).getAWTFont( getHUD() ) );
Rectangle2D bounds = metrics.getStringBounds( getDisplayedText().substring( 0, caretPosByChars ), graphics );
return ( (float)bounds.getWidth() );
}
private final void resetTextOffset()
{
setTextOffset( 0, 0 );
}
private void updateCaretPosition()
{
if ( getHUD() == null )
return;
caretLeftHUD = 0f;
if ( isEchoMode() )
{
caretLeftHUD += caretPosByChars * getEchoCharWidth();
}
else
{
caretLeftHUD += computeCaretPos();
}
if ( getDisplayedText().length() == 0 )
{
resetTextOffset();
}
else
{
int textOffset = getTextOffsetX();
int offsetIdx = -1;
while ( textOffset + caretLeftHUD < 0 )
{
textOffset += getCharWidth( ++offsetIdx );
}
int widthPX = getContentWidthPX();
offsetIdx = getText().length();
while ( textOffset + caretLeftHUD > widthPX )
{
textOffset -= getCharWidth( --offsetIdx );
}
setTextOffset( textOffset, getTextOffsetY() );
}
//System.out.println( getTextOffsetX() );
caretLeftHUD += getTextOffsetX();
final Dim2f buffer = Dim2f.fromPool();
caretLeftHUD = getSizePixels2HUD_( (int)caretLeftHUD, 0, buffer ).getWidth();
Dim2f pixel = buffer;
getSizeOfPixels_( 1, 1, pixel );
caretLeftHUD += getPaddingLeft();
if ( getBorder() != null )
caretLeftHUD += getBorder().getLeftWidth();
//getWidgetAssembler().reposition( blinker.caretImage, caretLeft, ( getContentTopPX() + 1 ) * pixel.getHeight() );
Dim2f.toPool( buffer );
updateTranslation();
}
public void setCaretPosition( int pos )
{
this.caretPosByChars = pos;
caretPosByChars = Math.max( caretPosByChars, 0 );
caretPosByChars = Math.min( caretPosByChars, getDisplayedText().length() );
updateCaretPosition();
}
public final int getCaretPosition()
{
return ( caretPosByChars );
}
/**
* Sets the character to replace each character of the TextField's text with
* when in echo-mode.
*
* @param echoChar
*/
public void setEchoChar( char echoChar )
{
this.echoChar = echoChar;
}
/**
* @return the character to replace each character of the TextField's text with
* when in echo-mode.
*/
public final char getEchoChar()
{
return ( echoChar );
}
/**
* Enables/Disables the echo-mode.<br>
* In echo-mode each character of the TextField's text will be replaced with
* the echo char.
*
* @param echoMode
* @param echoChar
*/
public void setEchoMode( boolean echoMode, char echoChar )
{
this.echoMode = echoMode;
setEchoChar( echoChar );
updateText();
}
/**
* Enables/Disables the echo-mode.<br>
* In echo-mode each character of the TextField's text will be replaced with
* the echo char.
*
* @param echoMode
*/
public void setEchoMode( boolean echoMode )
{
setEchoMode( echoMode, getEchoChar() );
}
/**
* @return the echo-mode.<br>
* In echo-mode each character of the TextField's text will be replaced with
* the echo char.
*/
public boolean isEchoMode()
{
return ( echoMode );
}
/**
* Sets the array of chars ignored by this TextField.
*
* @param ignoredChars
*/
public void setIgnoredChars( char... ignoredChars )
{
this.ignoredChars = ignoredChars;
}
/**
* @return the array of chars ignored by this TextField.
*/
public char[] getIgnoredChars()
{
return ( ignoredChars );
}
private void updateDisplayedText()
{
final String text = getText();
if ( isEchoMode() )
{
char[] chars = new char[ text.length() ];
for ( int i = 0; i < chars.length; i++ )
chars[i] = getEchoChar();
this.dispText = new String( chars );
}
else
{
this.dispText = text;
}
}
/**
* {@inheritDoc}
*/
@Override
public String getDisplayedText()
{
return ( dispText );
}
/**
* {@inheritDoc}
*/
@Override
protected void updateText()
{
updateDisplayedText();
super.updateText();
}
private void setTextInternal( String text )
{
super.setText( text );
updateCaretPosition();
}
/**
* {@inheritDoc}
*/
@Override
public void setText( String text )
{
//this.caretPosByChars = text.length();
this.caretPosByChars = 0;
resetTextOffset();
setTextInternal( text );
}
/**
* {@inheritDoc}
*/
@Override
public void setFont( HUDFont font )
{
super.setFont( font );
updateCaretPosition();
}
/**
* {@inheritDoc}
*/
@Override
protected boolean blocksFocusMoveDeviceComponent( DeviceComponent dc )
{
if ( dc == Keys.LEFT )
return ( true );
if ( dc == Keys.RIGHT )
return ( true );
return ( false );
}
/**
* {@inheritDoc}
*/
@Override
protected void onKeyPressed( Key key, int modifierMask, long when )
{
super.onKeyPressed( key, modifierMask, when );
if ( !isEditable() || !isEnabled() )
{
return;
}
switch ( key.getKeyID() )
{
case LEFT:
caretPosByChars = Math.max( 0, caretPosByChars - 1 );
updateCaretPosition();
setTextureDirty();
break;
case RIGHT:
caretPosByChars = Math.min( caretPosByChars + 1, getDisplayedText().length() );
updateCaretPosition();
setTextureDirty();
break;
case HOME:
caretPosByChars = 0;
updateCaretPosition();
setTextureDirty();
break;
case END:
caretPosByChars = getDisplayedText().length();
updateCaretPosition();
setTextureDirty();
break;
}
}
/**
* {@inheritDoc}
*/
@Override
protected void onKeyTyped( char ch, int modifierMask, long when )
{
super.onKeyTyped( ch, modifierMask, when );
if ( !isEditable() || !isEnabled() )
{
return;
}
if ( ch == '\b' )
{
if ( caretPosByChars > 0 )
{
final String leftPart = ( caretPosByChars > 0 ) ? getText().substring( 0, caretPosByChars - 1 ) : "";
final String rightPart = ( caretPosByChars < getText().length() ) ? getText().substring( caretPosByChars ) : "";
caretPosByChars -= 1;
setTextInternal( leftPart + rightPart );
setTextureDirty();
}
for ( int i = 0; i < listeners.size(); i++ )
{
listeners.get( i ).onCharDeleted( this );
}
}
else if ( ch == (char)127 ) // delete
{
if ( caretPosByChars < getText().length() )
{
final String leftPart = ( caretPosByChars > 0 ) ? getText().substring( 0, caretPosByChars ) : "";
final String rightPart = ( caretPosByChars < getText().length() ) ? getText().substring( caretPosByChars + 1 ) : "";
setTextInternal( leftPart + rightPart );
setTextureDirty();
}
for ( int i = 0; i < listeners.size(); i++ )
{
listeners.get( i ).onCharDeleted( this );
}
}
else if ( ch == (char)27 ) // ESCAPE
{
for ( int i = 0; i < listeners.size(); i++ )
{
listeners.get( i ).onEscapeHit( this );
}
}
else if ( ch == '\t' )
{
for ( int i = 0; i < listeners.size(); i++ )
{
listeners.get( i ).onTabHit( this );
}
}
else if ( ( ch == 10 ) || ( ch == 13 ) ) // ENTER
{
for ( int i = 0; i < listeners.size(); i++ )
{
listeners.get( i ).onEnterHit( this );
}
}
else
{
boolean ignored = false;
if ( ignoredChars != null )
{
for ( int i = 0; i < ignoredChars.length; i++ )
{
if ( ignoredChars[ i ] == ch )
{
ignored = true;
break;
}
}
}
if ( !ignored )
{
final String leftPart = ( caretPosByChars > 0 ) ? getText().substring( 0, caretPosByChars ) : "";
final String rightPart = ( caretPosByChars < getText().length() ) ? getText().substring( caretPosByChars ) : "";
caretPosByChars += 1;
setTextInternal( leftPart + ch + rightPart );
//setTextureDirty();
for ( int i = 0; i < listeners.size(); i++ )
{
listeners.get( i ).onCharTyped( this, ch );
}
}
}
}
/**
* {@inheritDoc}
*/
@Override
protected void onSizeChanged( float oldWidth, float oldHeight, float newWidth, float newHeight )
{
super.onSizeChanged( oldWidth, oldHeight, newWidth, newHeight );
updateCaretPosition();
}
/**
* {@inheritDoc}
*/
@Override
protected void onFocusGained()
{
super.onFocusGained();
if ( isEditable() )
{
blinker.start( this, getHUD().getOperationScheduler() );
}
}
/**
* {@inheritDoc}
*/
@Override
protected void onFocusLost()
{
super.onFocusLost();
blinker.stop();
}
/**
* {@inheritDoc}
*/
@Override
protected void onAttachedToHUD( HUD hud )
{
super.onAttachedToHUD( hud );
updateCaretPosition();
}
/**
* {@inheritDoc}
*/
@Override
protected void onDetachedFromHUD( HUD hud )
{
super.onDetachedFromHUD( hud );
if ( blinker != null )
{
blinker.stop();
}
}
@Override
protected void drawWidget( Texture2DCanvas texCanvas, int offsetX, int offsetY, int width, int height, boolean drawsSelf )
{
super.drawWidget( texCanvas, offsetX, offsetY, width, height, drawsSelf );
if ( blinker.caretImage.isVisible() )
{
Dim2i buffer = Dim2i.fromPool();
getSizeHUD2Pixels_( caretLeftHUD, 0, buffer );
int caretLeftPX = buffer.getWidth();
Dim2i.toPool( buffer );
int caretWidth = HUDTextureUtils.getTextureWidth( caretTex );
DrawUtils.drawImage( null, caretTex, TileMode.STRETCH, texCanvas, offsetX + caretLeftPX - getPaddingLeft(), offsetY + 1, caretWidth, getContentHeightPX() - 2 );
}
}
/**
* {@inheritDoc}
*/
@Override
protected void init()
{
super.init();
updateDisplayedText();
setText( getText() );
}
/**
* Creates a new Label with the given width, height and z-index.
*
* @param isHeavyWeight
* @param width the new width of this Widget
* @param height the new height of this Widget
* @param text the text to display in this TextWidget
* @param textFieldDesc a LabelDescription instance holding information
* about font, color, alignment and background-texture
*/
public TextField( boolean isHeavyWeight, float width, float height, String text, Description textFieldDesc )
{
super( isHeavyWeight, true, width, height, text, textFieldDesc );
this.setFocussable( true );
//setPadding( 0, 0, 0, 2 );
setBorder( textFieldDesc.getBorderDescription() );
this.caretTex = ( textFieldDesc.getCaretTexture() != null ) ? textFieldDesc.getCaretTexture() : HUD.getTheme().getTextFieldDescription().getCaretTexture();
//this.caretPosByChars = getText().length();
this.caretPosByChars = 0;
resetTextOffset();
this.setCursor( Cursor.Type.TEXT );
}
private static Description getDesc( HUDFont font, Colorf color, TextAlignment alignment )
{
Description desc = HUD.getTheme().getTextFieldDescription();
if ( font != null )
{
desc.setFont( font, false );
desc.setFont( Label.Description.deriveDisabledFont( font ), false );
}
if ( color != null )
desc.setFontColor( color, false );
if ( alignment != null )
desc.setAlignment( alignment );
return ( desc );
}
/**
* Creates a new TextField with the given width, height and z-index.
*
* @param isHeavyWeight
* @param width the new width of this Widget
* @param height the new height of this Widget
* @param text the text to display in this TextWidget
* @param font the Font to be used for the text
* @param color the color to be used
* @param alignment the horizontal and vertical alignment
*/
public TextField( boolean isHeavyWeight, float width, float height, String text, HUDFont font, Colorf color, TextAlignment alignment )
{
this( isHeavyWeight, width, height, text, getDesc( font, color, alignment ) );
}
/**
* Creates a new TextField with the given width and height and no text
* initially.
*
* @param isHeavyWeight
* @param width the new width of this Widget
* @param height the new height of this Widget
* @param text the text to display in this TextWidget
*/
public TextField( boolean isHeavyWeight, float width, float height, String text )
{
this( isHeavyWeight, width, height, text, HUD.getTheme().getTextFieldDescription() );
}
/**
* Creates a new TextField with the given width and height and no text
* initially.
*
* @param isHeavyWeight
* @param width the new width of this Widget
* @param height the new height of this Widget
*/
public TextField( boolean isHeavyWeight, float width, float height )
{
this( isHeavyWeight, width, height, "", HUD.getTheme().getTextFieldDescription() );
}
/**
* Creates a new Label with the given width, height and z-index.
*
* @param width the new width of this Widget
* @param height the new height of this Widget
* @param text the text to display in this TextWidget
* @param textFieldDesc a LabelDescription instance holding information
* about font, color, alignment and background-texture
*/
public TextField( float width, float height, String text, Description textFieldDesc )
{
this( DEFAULT_HEAVYWEIGHT, width, height, text, textFieldDesc );
}
/**
* Creates a new TextField with the given width, height and z-index.
*
* @param width the new width of this Widget
* @param height the new height of this Widget
* @param text the text to display in this TextWidget
* @param font the Font to be used for the text
* @param color the color to be used
* @param alignment the horizontal and vertical alignment
*/
public TextField( float width, float height, String text, HUDFont font, Colorf color, TextAlignment alignment )
{
this( DEFAULT_HEAVYWEIGHT, width, height, text, getDesc( font, color, alignment ) );
}
/**
* Creates a new TextField with the given width and height and no text
* initially.
*
* @param width the new width of this Widget
* @param height the new height of this Widget
* @param text the text to display in this TextWidget
*/
public TextField( float width, float height, String text )
{
this( DEFAULT_HEAVYWEIGHT, width, height, text, HUD.getTheme().getTextFieldDescription() );
}
/**
* Creates a new TextField with the given width and height and no text
* initially.
*
* @param width the new width of this Widget
* @param height the new height of this Widget
*/
public TextField( float width, float height )
{
this( DEFAULT_HEAVYWEIGHT, width, height, "", HUD.getTheme().getTextFieldDescription() );
}
}