/**
* Copyright (C) 2009-2014 Cars and Tracks Development Project (CTDP).
*
* 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.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
package net.ctdp.rfdynhud.properties;
import net.ctdp.rfdynhud.values.AbstractSize;
import net.ctdp.rfdynhud.values.RelativePositioning;
import net.ctdp.rfdynhud.widgets.base.widget.Widget;
import net.ctdp.rfdynhud.widgets.base.widget.__WPrivilegedAccess;
/**
* The {@link Position} class is an abstraction of a positional value tuple.
* It can be used with percentual values or absolute pixels and can be global or Widget local.
*
* @author Marvin Froehlich (CTDP)
*/
public class Position
{
private static final float PIXEL_OFFSET = 10f;
private static final float PIXEL_OFFSET_CHECK_POSITIVE = +PIXEL_OFFSET - 0.001f;
private static final float PIXEL_OFFSET_CHECK_NEGATIVE = -PIXEL_OFFSET + 0.001f;
private RelativePositioning positioning;
private float x;
private float y;
private int bakedX = -1;
private int bakedY = -1;
private final AbstractSize size;
/*final*/ Widget widget;
private final boolean isGlobalPosition;
public final Widget getWidget()
{
return ( widget );
}
public final boolean isGlobalPosition()
{
return ( isGlobalPosition );
}
public final RelativePositioning getPositioning()
{
return ( positioning );
}
/**
* Gets the current x-location of this Widget.
*
* @see #getPositioning()
*
* @return the current x-location of this Widget.
*/
private final float getX()
{
return ( x );
}
/**
* Gets the current y-location of this Widget.
*
* @see #getPositioning()
*
* @return the current y-location of this Widget.
*/
private final float getY()
{
return ( y );
}
private static final boolean isNegPixelValue( float v )
{
return ( v < PIXEL_OFFSET_CHECK_NEGATIVE );
}
private static final boolean isPosPixelValue( float v )
{
return ( v > PIXEL_OFFSET_CHECK_POSITIVE );
}
private static final boolean isPixelValue( float v )
{
return ( ( v < PIXEL_OFFSET_CHECK_NEGATIVE ) || ( v > PIXEL_OFFSET_CHECK_POSITIVE ) );
}
private final float getScaleWidth()
{
if ( isGlobalPosition )
{
if ( widget.getMasterWidget() == null )
return ( widget.getConfiguration().getGameResolution().getViewportWidth() );
return ( widget.getMasterWidget().getInnerSize().getEffectiveWidth() );
}
return ( widget.getInnerSize().getEffectiveWidth() );
}
private final float getScaleHeight()
{
if ( isGlobalPosition )
{
if ( widget.getMasterWidget() == null )
return ( widget.getConfiguration().getGameResolution().getViewportHeight() );
return ( widget.getMasterWidget().getInnerSize().getEffectiveHeight() );
}
return ( widget.getInnerSize().getEffectiveHeight() );
}
private final float getHundretPercentWidth()
{
if ( isGlobalPosition )
{
if ( widget.getMasterWidget() == null )
return ( widget.getConfiguration().getGameResolution().getViewportHeight() * 4 / 3 );
return ( widget.getMasterWidget().getInnerSize().getEffectiveWidth() );
}
return ( widget.getInnerSize().getEffectiveWidth() );
}
/**
* Sets this Widget's position.
*
* @param positioning
* @param x
* @param y
*
* @return changed?
*/
private boolean set( RelativePositioning positioning, float x, float y )
{
if ( widget.getConfiguration() != null )
{
if ( positioning.isHCenter() )
{
/*
if ( isNegPixelValue( x ) )
x = Math.max( -PIXEL_OFFSET - getScaleWidth() / 2f + ( isGlobalPosition ? size.getEffectiveWidth() / 2f : 0f ), x );
else if ( isPosPixelValue( x ) )
x = Math.min( +PIXEL_OFFSET + getScaleWidth() / 2f - ( isGlobalPosition ? size.getEffectiveWidth() / 2f : 0f ), x );
else if ( x < 0f )
x = Math.max( -0.5f + ( isGlobalPosition ? size.getEffectiveWidth() / 2f / getHundretPercentWidth() : 0f ), x );
else if ( x > 0f )
x = Math.min( +0.5f - ( isGlobalPosition ? size.getEffectiveWidth() / 2f / getHundretPercentWidth() : 0f ), x );
*/
}
else if ( isPixelValue( x ) )
{
x = Math.max( PIXEL_OFFSET, x );
}
else
{
x = Math.max( 0f, x );
}
if ( positioning.isVCenter() )
{
/*
if ( isNegPixelValue( y ) )
y = Math.max( -PIXEL_OFFSET - getScaleHeight() / 2f + ( isGlobalPosition ? size.getEffectiveHeight() / 2f : 0f ), y );
else if ( isPosPixelValue( y ) )
y = Math.min( +PIXEL_OFFSET + getScaleHeight() / 2f - ( isGlobalPosition ? size.getEffectiveHeight() / 2f : 0f ), y );
else if ( y < 0f )
y = Math.max( -0.5f + ( isGlobalPosition ? size.getEffectiveHeight() / 2f / getScaleHeight() : 0f ), y );
else if ( y > 0f )
y = Math.min( +0.5f - ( isGlobalPosition ? size.getEffectiveHeight() / 2f / getScaleHeight() : 0f ), y );
*/
}
else if ( isPixelValue( y ) )
{
y = Math.max( PIXEL_OFFSET, y );
}
else
{
y = Math.max( 0f, y );
}
}
unbake();
boolean changed = false;
if ( ( positioning != this.positioning ) || ( x != this.x ) || ( y != this.y ) )
{
RelativePositioning oldPositioning = this.positioning;
boolean b = ( widget.getConfiguration() != null );
int oldX = b ? getEffectiveX() : 0;
int oldY = b ? getEffectiveY() : 0;
this.positioning = positioning;
this.x = x;
this.y = y;
// Since we're no longer drawing everything on one big texture now, we don't need this anymore.
//if ( !__EDPrivilegedAccess.isEditorMode )
// widget.forceCompleteRedraw( true );
widget.setDirtyFlag();
if ( b )
{
int newX = getEffectiveX();
int newY = getEffectiveY();
if ( oldX != newX || oldY != newY )
__WPrivilegedAccess.onPositionChanged( oldPositioning, oldX, oldY, positioning, newX, newY, widget );
}
changed = true;
}
return ( changed );
}
/**
* Sets this Widget's position.
*
* @param x
* @param y
*/
private final boolean set( float x, float y )
{
return ( set( getPositioning(), x, y ) );
}
/**
* Sets this Widget's x-position.
*
* @param x
*/
private final boolean setX( float x )
{
return ( set( getPositioning(), x, getY() ) );
}
/**
* Sets this Widget's y-position.
*
* @param y
*/
private final boolean setY( float y )
{
return ( set( getPositioning(), getX(), y ) );
}
/**
* Sets the {@link Position} to the values of the given {@link Position}.
*
* @param position
*/
public void setTo( Position position )
{
this.set( position.positioning, position.x, position.y );
}
/**
* Sets this Widget's position.
*
* @param positioning the used {@link RelativePositioning}
* @param x the absolute pixel x
* @param y the absolute pixel y
*
* @return changed?
*/
public final boolean setEffectivePosition( RelativePositioning positioning, int x, int y )
{
float scaleW = getScaleWidth();
float scaleH = getScaleHeight();
if ( isGlobalPosition && !isPixelValue( this.x ) )
{
if ( positioning.isRight() )
x = (int)Math.max( scaleW - getHundretPercentWidth() - widget.getSize().getEffectiveWidth(), x );
else
x = (int)Math.min( x, getHundretPercentWidth() );
}
if ( positioning.isVCenter() )
y = y + ( size.getEffectiveHeight() - (int)scaleH ) / 2;
else if ( positioning.isBottom() )
y = (int)scaleH - y - size.getEffectiveHeight();
if ( positioning.isHCenter() )
x = x + ( size.getEffectiveWidth() - (int)scaleW ) / 2;
else if ( positioning.isRight() )
x = (int)scaleW - x - size.getEffectiveWidth();
float newX, newY;
if ( isPixelValue( this.x ) )
{
if ( isPixelValue( this.y ) )
{
newX = ( x < 0 ? -PIXEL_OFFSET : +PIXEL_OFFSET ) + x;
newY = ( y < 0 ? -PIXEL_OFFSET : +PIXEL_OFFSET ) + y;
}
else
{
newX = ( x < 0 ? -PIXEL_OFFSET : +PIXEL_OFFSET ) + x;
newY = y / scaleH;
}
}
else if ( isPixelValue( this.y ) )
{
newX = x / getHundretPercentWidth();
newY = ( y < 0 ? -PIXEL_OFFSET : +PIXEL_OFFSET ) + y;
}
else
{
newX = x / getHundretPercentWidth();
newY = y / scaleH;
}
return ( set( positioning, newX, newY ) );
}
/**
* Sets this Widget's position.
*
* @param x the absolute pixel x
* @param y the absolute pixel y
*
* @return changed?
*/
public final boolean setEffectivePosition( int x, int y )
{
return ( setEffectivePosition( getPositioning(), x, y ) );
}
/**
* Gets the effective Widget's x-location using {@link #getPositioning()}.
*
* @return the effective Widget's x-location.
*/
public final int getEffectiveX()
{
if ( bakedX >= 0 )
return ( bakedX );
float scaleW = getScaleWidth();
switch ( getPositioning() )
{
case TOP_LEFT:
case CENTER_LEFT:
case BOTTOM_LEFT:
if ( isPosPixelValue( x ) )
return ( (int)( x - PIXEL_OFFSET ) );
if ( isNegPixelValue( x ) )
return ( (int)( x + PIXEL_OFFSET ) );
return ( Math.round( x * getHundretPercentWidth() ) );
case TOP_CENTER:
case CENTER_CENTER:
case BOTTOM_CENTER:
if ( isPosPixelValue( x ) )
return ( ( (int)scaleW - size.getEffectiveWidth() ) / 2 + (int)( x - PIXEL_OFFSET ) );
if ( isNegPixelValue( x ) )
return ( ( (int)scaleW - size.getEffectiveWidth() ) / 2 + (int)( x + PIXEL_OFFSET ) );
return ( ( (int)scaleW - size.getEffectiveWidth() ) / 2 + Math.round( x * getHundretPercentWidth() ) );
case TOP_RIGHT:
case CENTER_RIGHT:
case BOTTOM_RIGHT:
if ( isPosPixelValue( x ) )
return ( (int)scaleW - (int)( x - PIXEL_OFFSET ) - size.getEffectiveWidth() );
if ( isNegPixelValue( x ) )
return ( (int)scaleW - (int)( x + PIXEL_OFFSET ) - size.getEffectiveWidth() );
return ( (int)scaleW - Math.round( x * getHundretPercentWidth() ) - size.getEffectiveWidth() );
}
// Unreachable code!
return ( -1 );
}
/**
* Gets the effective Widget's y-location using {@link #getPositioning()}.
*
* @return the effective Widget's y-location.
*/
public final int getEffectiveY()
{
if ( bakedY >= 0 )
return ( bakedY );
float scaleH = getScaleHeight();
switch ( getPositioning() )
{
case TOP_LEFT:
case TOP_CENTER:
case TOP_RIGHT:
if ( isPosPixelValue( y ) )
return ( (int)( y - PIXEL_OFFSET ) );
if ( isNegPixelValue( y ) )
return ( (int)( y + PIXEL_OFFSET ) );
return ( Math.round( y * scaleH ) );
case CENTER_LEFT:
case CENTER_CENTER:
case CENTER_RIGHT:
if ( isPosPixelValue( y ) )
return ( ( (int)scaleH - size.getEffectiveHeight() ) / 2 + (int)( y - PIXEL_OFFSET ) );
if ( isNegPixelValue( y ) )
return ( ( (int)scaleH - size.getEffectiveHeight() ) / 2 + (int)( y + PIXEL_OFFSET ) );
return ( ( (int)scaleH - size.getEffectiveHeight() ) / 2 + Math.round( y * scaleH ) );
case BOTTOM_LEFT:
case BOTTOM_CENTER:
case BOTTOM_RIGHT:
if ( isPosPixelValue( y ) )
return ( (int)scaleH - (int)( y - PIXEL_OFFSET ) - size.getEffectiveHeight() );
if ( isNegPixelValue( y ) )
return ( (int)scaleH - (int)( y + PIXEL_OFFSET ) - size.getEffectiveHeight() );
return ( Math.round( scaleH - ( y * scaleH ) - size.getEffectiveHeight() ) );
}
// Unreachable code!
return ( -1 );
}
public final boolean equalsEffective( int x, int y )
{
return ( ( getEffectiveX() == x ) && ( getEffectiveY() == y ) );
}
public void unbake()
{
bakedX = -1;
bakedY = -1;
}
public void bake()
{
boolean isSizeBaked = false;
if ( size instanceof Size )
{
isSizeBaked = ( (Size)size ).isBaked();
( (Size)size ).unbake();
}
unbake();
bakedX = getEffectiveX();
bakedY = getEffectiveY();
if ( isSizeBaked )
{
( (Size)size ).bake();
}
}
public final boolean isBaked()
{
return ( bakedX >= 0 );
}
public Position setXToPercents()
{
if ( isPixelValue( x ) )
{
int effX = getEffectiveX();
int effY = getEffectiveY();
if ( x > 0f )
this.x = +PIXEL_OFFSET * 0.9f;
else
this.x = -PIXEL_OFFSET * 0.9f;
setEffectivePosition( getPositioning(), effX, effY );
}
return ( this );
}
public Position setXToPixels()
{
if ( !isPixelValue( x ) )
{
int effX = getEffectiveX();
int effY = getEffectiveY();
if ( x > 0f )
this.x = +PIXEL_OFFSET + 10000f;
else
this.x = -PIXEL_OFFSET - 10000f;
setEffectivePosition( getPositioning(), effX, effY );
}
return ( this );
}
public Position flipXPercentagePx()
{
if ( isPixelValue( x ) )
setXToPercents();
else
setXToPixels();
return ( this );
}
public Position setYToPercents()
{
if ( isPixelValue( y ) )
{
int effX = getEffectiveX();
int effY = getEffectiveY();
if ( y > 0f )
this.y = +PIXEL_OFFSET * 0.9f;
else
this.y = -PIXEL_OFFSET * 0.9f;
setEffectivePosition( getPositioning(), effX, effY );
}
return ( this );
}
public Position setYToPixels()
{
if ( !isPixelValue( y ) )
{
int effX = getEffectiveX();
int effY = getEffectiveY();
if ( y > 0f )
this.y = +PIXEL_OFFSET + 10000f;
else
this.y = -PIXEL_OFFSET - 10000f;
setEffectivePosition( getPositioning(), effX, effY );
}
return ( this );
}
public Position flipYPercentagePx()
{
if ( isPixelValue( y ) )
setYToPercents();
else
setYToPixels();
return ( this );
}
public static float parseValue( String value, boolean defaultPerc )
{
boolean isPerc = value.endsWith( "%" );
boolean isPx = value.endsWith( "px" );
if ( !isPerc && !isPx )
{
if ( defaultPerc )
{
value += "%";
isPerc = true;
}
else
{
value += "px";
isPx = true;
}
}
if ( isPerc )
{
float f = Float.parseFloat( value.substring( 0, value.length() - 1 ) );
return ( f / 100f );
}
if ( isPx )
{
float f = Float.parseFloat( value.substring( 0, value.length() - 2 ) );
if ( f < 0f )
return ( -PIXEL_OFFSET + f );
return ( +PIXEL_OFFSET + f );
}
// Unreachable!
return ( Float.parseFloat( value ) );
}
/*
private float parseX( String value )
{
setX( parseValue( value ) );
return ( getX() );
}
private float parseY( String value )
{
setY( parseValue( value ) );
return ( getY() );
}
*/
public static String unparseValue( float value )
{
if ( isPosPixelValue( value ) )
return ( String.valueOf( (int)( value - PIXEL_OFFSET ) ) + "px" );
if ( isNegPixelValue( value ) )
return ( String.valueOf( (int)( value + PIXEL_OFFSET ) ) + "px" );
return ( String.valueOf( value * 100f ) + "%" );
}
/*
private String unparseX()
{
return ( unparseValue( getX() ) );
}
private String unparseY()
{
return ( unparseValue( getY() ) );
}
*/
private static final boolean propExistsWithName( Property prop, String name, String nameForDisplay )
{
if ( prop == null )
return ( false );
if ( !prop.getName().equals( name ) )
return ( false );
if ( ( nameForDisplay == null ) && !prop.getName().equals( prop.getNameForDisplay() ) )
return ( false );
return ( true );
}
/**
*
* @param positioning the new positioning
*/
protected void onPositioningPropertySet( RelativePositioning positioning )
{
}
private Property posProp = null;
public Property getPositioningProperty( String name, String nameForDisplay )
{
if ( !propExistsWithName( posProp, name, nameForDisplay ) )
{
posProp = new Property( name, nameForDisplay, PropertyEditorType.ENUM )
{
@Override
public void setValue( Object value )
{
if ( positioning == value )
return;
if ( ( Position.this.getWidget() != null ) && ( Position.this.getWidget().getConfiguration() != null ) )
{
RelativePositioning oldValue = positioning;
int currX = getEffectiveX();
int currY = getEffectiveY();
setEffectivePosition( (RelativePositioning)value, currX, currY );
triggerKeepersOnPropertyChanged( oldValue, value );
onPositioningPropertySet( (RelativePositioning)value );
}
else
{
Position.this.positioning = (RelativePositioning)value;
}
}
@Override
public Object getValue()
{
return ( getPositioning() );
}
/**
* {@inheritDoc}
*/
@Override
public Object getDefaultValue()
{
return ( null );
}
/**
* {@inheritDoc}
*/
@Override
public boolean hasDefaultValue()
{
return ( false );
}
@Override
public void loadValue( PropertyLoader loader, String value )
{
Position.this.set( RelativePositioning.valueOf( value ), getX(), getY() );
}
};
}
return ( posProp );
}
public final Property getPositioningProperty( String name )
{
return ( getPositioningProperty( name, null ) );
}
/**
*
* @param x the new x
*/
protected void onXPropertySet( float x )
{
}
private PosSizeProperty xProp = null;
public PosSizeProperty getXProperty( String name, String nameForDisplay )
{
if ( !propExistsWithName( xProp, name, nameForDisplay ) )
{
xProp = new PosSizeProperty( name, nameForDisplay, false, false )
{
@Override
public boolean isPercentage()
{
return ( !isPixelValue( x ) );
}
@Override
public void setValue( Object value )
{
float oldValue = Position.this.x;
float x = ( (Number)value ).floatValue();
//if ( x != oldValue )
{
set( x, getY() );
if ( x != oldValue )
triggerKeepersOnPropertyChanged( oldValue, value );
onXPropertySet( x );
}
}
@Override
public Object getValue()
{
return ( getX() );
}
/**
* {@inheritDoc}
*/
@Override
public Object getDefaultValue()
{
return ( null );
}
/**
* {@inheritDoc}
*/
@Override
public boolean hasDefaultValue()
{
return ( false );
}
@Override
public void onButtonClicked( Object button )
{
flipXPercentagePx();
}
@Override
public Boolean quoteValueInConfigurationFile()
{
return ( false );
}
@Override
public Object getValueForConfigurationFile()
{
return ( unparseValue( getX() ) );
}
@Override
public void loadValue( PropertyLoader loader, String value )
{
if ( !value.endsWith( "%" ) && !value.endsWith( "px" ) )
value += "px";
setX( parseValue( value, !isPixelValue( x ) ) );
}
};
}
return ( xProp );
}
public final PosSizeProperty getXProperty( String name )
{
return ( getXProperty( name, null ) );
}
/**
*
* @param y the new y
*/
protected void onYPropertySet( float y )
{
}
private PosSizeProperty yProp = null;
public PosSizeProperty getYProperty( String name, String nameForDisplay )
{
if ( !propExistsWithName( yProp, name, nameForDisplay ) )
{
yProp = new PosSizeProperty( name, nameForDisplay, false, false )
{
@Override
public boolean isPercentage()
{
return ( !isPixelValue( y ) );
}
@Override
public void setValue( Object value )
{
float oldValue = Position.this.y;
float y = ( (Number)value ).floatValue();
//if ( y != oldValue )
{
set( getX(), y );
if ( y != oldValue )
triggerKeepersOnPropertyChanged( oldValue, value );
onYPropertySet( y );
}
}
@Override
public Object getValue()
{
return ( getY() );
}
/**
* {@inheritDoc}
*/
@Override
public Object getDefaultValue()
{
return ( null );
}
/**
* {@inheritDoc}
*/
@Override
public boolean hasDefaultValue()
{
return ( false );
}
@Override
public void onButtonClicked( Object button )
{
flipYPercentagePx();
}
@Override
public Boolean quoteValueInConfigurationFile()
{
return ( false );
}
@Override
public Object getValueForConfigurationFile()
{
return ( unparseValue( getY() ) );
}
@Override
public void loadValue( PropertyLoader loader, String value )
{
if ( !value.endsWith( "%" ) && !value.endsWith( "px" ) )
value += "px";
setY( parseValue( value, !isPixelValue( y ) ) );
}
};
}
return ( yProp );
}
public final PosSizeProperty getYProperty( String name )
{
return ( getYProperty( name, null ) );
}
protected Position( Widget widget, boolean isGlobalPosition, RelativePositioning positioning, float x, boolean xPercent, float y, boolean yPercent, AbstractSize size )
{
this.widget = widget;
this.isGlobalPosition = isGlobalPosition;
this.positioning = positioning;
this.x = xPercent ? x * 0.01f : PIXEL_OFFSET + x;
this.y = yPercent ? y * 0.01f : PIXEL_OFFSET + y;
this.size = size;
}
/**
* Create a new positional property for positions local to a Widget's area.
*
* @param widget the owning {@link Widget}.
* @param positioning the used {@link RelativePositioning}
* @param x the x position
* @param xPercent interpret 'x' as percents?
* @param y the y position
* @param yPercent interpret 'y' as percents?
* @param size the size for the area
*
* @return the new Position.
*/
public static final Position newLocalPosition( Widget widget, RelativePositioning positioning, float x, boolean xPercent, float y, boolean yPercent, AbstractSize size )
{
return ( new Position( widget, false, positioning, x, xPercent, y, yPercent, size ) );
}
/**
* Create a new positional property for global positions on the whole screen area.
*
* @param widget the owning {@link Widget}.
* @param positioning the used {@link RelativePositioning}
* @param x the x position
* @param xPercent interpret 'x' as percents?
* @param y the y position
* @param yPercent interpret 'y' as percents?
* @param size the size for the area
*
* @return the new Position.
*/
public static final Position newGlobalPosition( Widget widget, RelativePositioning positioning, float x, boolean xPercent, float y, boolean yPercent, AbstractSize size )
{
return ( new Position( widget, true, positioning, x, xPercent, y, yPercent, size ) );
}
}