/**
* Copyright (c) 2008-2012 Ardor Labs, Inc.
*
* This file is part of Ardor3D.
*
* Ardor3D is free software: you can redistribute it and/or modify it
* under the terms of its license which may be found in the accompanying
* LICENSE file or at <http://www.ardor3d.com/LICENSE>.
*/
package com.ardor3d.extension.ui;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
import com.ardor3d.extension.ui.backdrop.EmptyBackdrop;
import com.ardor3d.extension.ui.backdrop.UIBackdrop;
import com.ardor3d.extension.ui.border.EmptyBorder;
import com.ardor3d.extension.ui.border.UIBorder;
import com.ardor3d.extension.ui.layout.UILayoutData;
import com.ardor3d.extension.ui.skin.SkinManager;
import com.ardor3d.extension.ui.text.StyleConstants;
import com.ardor3d.extension.ui.text.UIKeyHandler;
import com.ardor3d.extension.ui.util.Dimension;
import com.ardor3d.extension.ui.util.Insets;
import com.ardor3d.input.InputState;
import com.ardor3d.input.Key;
import com.ardor3d.input.MouseButton;
import com.ardor3d.math.ColorRGBA;
import com.ardor3d.math.MathUtils;
import com.ardor3d.math.Rectangle2;
import com.ardor3d.math.Transform;
import com.ardor3d.math.Vector3;
import com.ardor3d.math.type.ReadOnlyColorRGBA;
import com.ardor3d.math.type.ReadOnlyTransform;
import com.ardor3d.math.type.ReadOnlyVector3;
import com.ardor3d.renderer.Camera;
import com.ardor3d.renderer.ContextManager;
import com.ardor3d.renderer.Renderer;
import com.ardor3d.renderer.state.BlendState;
import com.ardor3d.renderer.state.BlendState.BlendEquation;
import com.ardor3d.renderer.state.BlendState.DestinationFunction;
import com.ardor3d.renderer.state.BlendState.SourceFunction;
import com.ardor3d.renderer.state.RenderState.StateType;
import com.ardor3d.scenegraph.Node;
import com.ardor3d.scenegraph.event.DirtyType;
import com.ardor3d.scenegraph.hint.PickingHint;
import com.google.common.collect.Maps;
/**
* Base UI class. All UI components/widgets/controls extend this class.
*
* TODO: alert/dirty needed for font style changes.
*/
public abstract class UIComponent extends Node implements UIKeyHandler {
/** If true, use opacity settings to blend the components. Default is false. */
private static boolean _useTransparency = false;
/** The opacity of the component currently being rendered. */
private static float _currentOpacity = 1f;
/** The internal contents portion of this component. */
private final Dimension _contentsSize = new Dimension(10, 10);
/** The absolute minimum size of the internal contents portion of this component. */
private final Dimension _minimumContentsSize = new Dimension(10, 10);
/** The absolute maximum size of the internal contents portion of this component. */
private final Dimension _maximumContentsSize = new Dimension(10000, 10000);
/** A spacing between the component's border and its inner content area. */
private Insets _padding = new Insets(0, 0, 0, 0);
/** A border around this component. */
private UIBorder _border = new EmptyBorder();
/** A spacing between the component's border and other content outside this component. Used during layout. */
private Insets _margin = new Insets(0, 0, 0, 0);
/** The renderer responsible for drawing this component's backdrop. */
private UIBackdrop _backdrop = new EmptyBackdrop();
/** White */
public static ReadOnlyColorRGBA DEFAULT_FOREGROUND_COLOR = ColorRGBA.WHITE;
/** The color of any text or other such foreground elements. Inherited from parent if null. */
private ColorRGBA _foregroundColor = null;
/** Text to display using UITooltip when hovered over. Inherited from parent if null. */
protected String _tooltipText = null;
/** The amount of time (in ms) before we should show the tooltip on this component. */
protected int _tooltipPopTime = 1000;
/** The default font family (Vera) used when font family field and all parents font fields are null. */
private static String _defaultFontFamily = "Vera";
/** The default font size (18) used when font size field and all parents font size fields are 0. */
private static int _defaultFontSize = 18;
/** The default font styles to use. */
private static Map<String, Object> _defaultFontStyles = Maps.newHashMap();
/** The font styles to use for text on this component, if needed. */
private Map<String, Object> _fontStyles = Maps.newHashMap();
/** Optional information used by a parent container's layout. */
private UILayoutData _layoutData = null;
/** If true, this component is drawn. */
private boolean _visible = true;
/** If false, this component may optionally disable input to it and its children (as applicable). */
private boolean _enabled = true;
/** If true, we will consume any mouse events that are sent to this component. (default is false) */
private boolean _consumeMouseEvents = false;
/** If true, we will consume any key events that are sent to this component. (default is false) */
private boolean _consumeKeyEvents = false;
/** The opacity of this component. 0 is fully transparent and 1 is fully opaque. The default is 1. */
private float _opacity = 1.0f;
/** If we are selected for key focus, we'll redirect that focus to this target if not null. */
private UIComponent _keyFocusTarget = null;
/**
* flag to indicate if a component has touched its content area. Used to determine blending operations for child
* components.
*/
private boolean _virginContentArea = false;
/** The current future task set to show a tooltip in the near future. */
private FutureTask<Void> _showTask;
/** The system time when the tool tip should show up next (if currently being timed.) */
private long _toolDone;
/** Blend states to use when drawing components as cached container contents. */
private static BlendState _srcRGBmaxAlphaBlend = UIComponent.createSrcRGBMaxAlphaBlend();
private static BlendState _blendRGBmaxAlphaBlend = UIComponent.createBlendRGBMaxAlphaBlend();
protected void applySkin() {
SkinManager.applyCurrentSkin(this);
updateMinimumSizeFromContents();
}
/**
* @return true if this component should be considered "enabled"... a concept that is interpreted by each individual
* component type.
*/
public boolean isEnabled() {
return _enabled;
}
/**
* @param enabled
* true if this component should be considered "enabled"... a concept that is interpreted by each
* individual component type.
*/
public void setEnabled(final boolean enabled) {
_enabled = enabled;
}
/**
* @return true if this component should be drawn.
*/
public boolean isVisible() {
return _visible;
}
/**
* @param visible
* true if this component should be drawn.
*/
public void setVisible(final boolean visible) {
_visible = visible;
}
/**
* Used primarily during rendering to determine how alpha blending should be done.
*
* @return true if nothing has been drawn by this component or its ancestors yet that would affect its content area.
*/
public boolean hasVirginContentArea() {
if (!_virginContentArea) {
return false;
} else if (getParent() instanceof UIComponent) {
return ((UIComponent) getParent()).hasVirginContentArea();
} else {
return true;
}
}
/**
* @param virgin
* true if nothing has been drawn by this component yet that would affect its content area.
*/
public void setVirginContentArea(final boolean virgin) {
_virginContentArea = virgin;
}
/**
* Add a new font style to this component. Will be inherited by children if they do not have the same key.
*
* @param styleKey
* style key
* @param styleValue
* the value to associate with the key.
*/
public void addFontStyle(final String styleKey, final Object styleValue) {
_fontStyles.put(styleKey, styleValue);
fireStyleChanged();
}
/**
* Removes a font style locally from this component, if present.
*
* @param styleKey
* style key
* @return the style value we were mapped to, or null if none (or was mapped to null).
*/
public Object clearFontStyle(final String styleKey) {
final Object removedValue = _fontStyles.remove(styleKey);
fireStyleChanged();
return removedValue;
}
/**
* @return the locally set font styles.
*/
public Map<String, Object> getLocalFontStyles() {
return _fontStyles;
}
/**
* @return our combined font styles, merged downward from root, including defaults.
*/
public Map<String, Object> getFontStyles() {
final Map<String, Object> styles;
if (getParent() != null && getParent() instanceof UIComponent) {
styles = ((UIComponent) getParent()).getFontStyles();
} else {
styles = Maps.newHashMap(UIComponent._defaultFontStyles);
styles.put(StyleConstants.KEY_COLOR, UIComponent.DEFAULT_FOREGROUND_COLOR);
}
styles.putAll(_fontStyles);
if (_foregroundColor != null) {
styles.put(StyleConstants.KEY_COLOR, _foregroundColor);
}
return styles;
}
/**
* @param styles
* the font styles to use (as needed) for this component. Note that this can be inherited by child
* components if this is a container class and styles is null.
*/
public void setFontStyles(final Map<String, Object> styles) {
if (styles == null) {
_fontStyles = null;
} else {
_fontStyles.clear();
_fontStyles.putAll(styles);
}
fireStyleChanged();
}
/**
* @return the set layout data object or null if none has been set.
*/
public UILayoutData getLayoutData() {
return _layoutData;
}
/**
* @param layoutData
* the layout data object set on this component. The object would provide specific layout directions to
* the layout class of the container this component is placed in.
*/
public void setLayoutData(final UILayoutData layoutData) {
_layoutData = layoutData;
}
/**
* @return the width of this entire component as a whole, including all margins, borders, padding and content (in
* the component's coordinate space.)
*/
public int getLocalComponentWidth() {
return _contentsSize.getWidth() + getTotalLeft() + getTotalRight();
}
/**
* @return the height of this entire component as a whole, including all margins, borders, padding and content (in
* the component's coordinate space.)
*/
public int getLocalComponentHeight() {
return _contentsSize.getHeight() + getTotalTop() + getTotalBottom();
}
/**
*
* @param store
* the object to store our results in. If null, a new Rectangle2 is created and returned.
* @return
*/
public Rectangle2 getRelativeMinComponentBounds(final Rectangle2 store) {
Rectangle2 rVal = store;
if (rVal == null) {
rVal = new Rectangle2();
}
final int height = getMinimumLocalComponentHeight();
final int width = getMinimumLocalComponentWidth();
return getRelativeComponentBounds(rVal, width, height);
}
/**
*
* @param store
* the object to store our results in. If null, a new Rectangle2 is created and returned.
* @return
*/
public Rectangle2 getRelativeMaxComponentBounds(final Rectangle2 store) {
Rectangle2 rVal = store;
if (rVal == null) {
rVal = new Rectangle2();
}
final int height = getMaximumLocalComponentHeight();
final int width = getMaximumLocalComponentWidth();
return getRelativeComponentBounds(rVal, width, height);
}
/**
*
* @param store
* the object to store our results in. If null, a new Rectangle2 is created and returned.
* @return the current bounds of this component, in the coordinate space of its parent.
*/
public Rectangle2 getRelativeComponentBounds(final Rectangle2 store) {
Rectangle2 rVal = store;
if (rVal == null) {
rVal = new Rectangle2();
}
final int height = getLocalComponentHeight();
final int width = getLocalComponentWidth();
return getRelativeComponentBounds(rVal, width, height);
}
private Rectangle2 getRelativeComponentBounds(final Rectangle2 store, final int width, final int height) {
final ReadOnlyTransform local = getTransform();
if (local.isIdentity() || local.getMatrix().isIdentity()) {
store.set(0, 0, width, height);
} else {
float minX, maxX, minY, maxY;
final Vector3 t = Vector3.fetchTempInstance();
t.set(width, height, 0);
local.applyForwardVector(t);
minX = Math.min(t.getXf(), 0);
maxX = Math.max(t.getXf(), 0);
minY = Math.min(t.getYf(), 0);
maxY = Math.max(t.getYf(), 0);
t.set(0, height, 0);
local.applyForwardVector(t);
minX = Math.min(t.getXf(), minX);
maxX = Math.max(t.getXf(), maxX);
minY = Math.min(t.getYf(), minY);
maxY = Math.max(t.getYf(), maxY);
t.set(width, 0, 0);
local.applyForwardVector(t);
minX = Math.min(t.getXf(), minX);
maxX = Math.max(t.getXf(), maxX);
minY = Math.min(t.getYf(), minY);
maxY = Math.max(t.getYf(), maxY);
Vector3.releaseTempInstance(t);
store.set(Math.round(minX), Math.round(minY), Math.round(maxX - minX), Math.round(maxY - minY));
}
return store;
}
/**
* Sets the width and height of this component by forcing the content area to be of a proper size such that when the
* padding, margin and border are added, the total component size match those given.
*
* @param width
* the new width of the component
* @param height
* the new height of the component
*/
public void setLocalComponentSize(final int width, final int height) {
setLocalComponentWidth(width);
setLocalComponentHeight(height);
}
/**
* @return the width contained in _minimumContentsSize + the margin, border and padding values for left and right.
*/
public int getMinimumLocalComponentWidth() {
return _minimumContentsSize.getWidth() + getTotalLeft() + getTotalRight();
}
/**
* @return the height contained in _minimumContentsSize + the margin, border and padding values for top and bottom.
*/
public int getMinimumLocalComponentHeight() {
return _minimumContentsSize.getHeight() + getTotalTop() + getTotalBottom();
}
/**
* @return the width contained in _maximumContentsSize + the margin, border and padding values for left and right.
*/
public int getMaximumLocalComponentWidth() {
return _maximumContentsSize.getWidth() + getTotalLeft() + getTotalRight();
}
/**
* @return the height contained in _maximumContentsSize + the margin, border and padding values for top and bottom.
*/
public int getMaximumLocalComponentHeight() {
return _maximumContentsSize.getHeight() + getTotalTop() + getTotalBottom();
}
/**
* Sets the width and height of the content area of this component.
*
* @param width
* the new width of the content area
* @param height
* the new height of the content area
*/
public void setContentSize(final int width, final int height) {
setContentWidth(width);
setContentHeight(height);
if (_maximumContentsSize.getWidth() < width) {
_maximumContentsSize.setWidth(width);
}
if (_maximumContentsSize.getHeight() < height) {
_maximumContentsSize.setHeight(height);
}
}
/**
* Sets the height of the content area of this component to that given, as long as we're between min and max content
* height.
*
* @param height
* the new height
*/
public void setContentHeight(final int height) {
_contentsSize.setHeight(MathUtils.clamp(height, _minimumContentsSize.getHeight(),
_maximumContentsSize.getHeight()));
}
/**
* Sets the width of the content area of this component to that given, as long as we're between min and max content
* width.
*
* @param width
* the new width
*/
public void setContentWidth(final int width) {
_contentsSize
.setWidth(MathUtils.clamp(width, _minimumContentsSize.getWidth(), _maximumContentsSize.getWidth()));
}
/**
* Sets the current component height to that given, as long as it would not violate min and max content height.
*
* @param height
* the new height
*/
public void setLocalComponentHeight(final int height) {
setContentHeight(height - getTotalTop() - getTotalBottom());
}
/**
* Sets the current component width to that given, as long as it would not violate min and max content width.
*
* @param width
* the new width
*/
public void setLocalComponentWidth(final int width) {
setContentWidth(width - getTotalLeft() - getTotalRight());
}
/**
* @return the width of the content area of this component.
*/
public int getContentWidth() {
return _contentsSize.getWidth();
}
/**
* @return the height of the content area of this component.
*/
public int getContentHeight() {
return _contentsSize.getHeight();
}
/**
* Sets the maximum content size of this component to the values given.
*
* @param width
* @param height
*/
public void setMaximumContentSize(final int width, final int height) {
_maximumContentsSize.set(width, height);
validateContentSize();
}
/**
* Sets the maximum content width of this component to the value given.
*
* @param width
*/
public void setMaximumContentWidth(final int width) {
_maximumContentsSize.setWidth(width);
}
/**
* Sets the maximum content height of this component to the value given.
*
* @param height
*/
public void setMaximumContentHeight(final int height) {
_maximumContentsSize.setHeight(height);
validateContentSize();
}
/**
* Sets the minimum content size of this component to the values given.
*
* @param width
* @param height
*/
public void setMinimumContentSize(final int width, final int height) {
_minimumContentsSize.set(width, height);
validateContentSize();
}
/**
* Sets the minimum content width of this component to the value given.
*
* @param width
*/
public void setMinimumContentWidth(final int width) {
_minimumContentsSize.setWidth(width);
validateContentSize();
}
/**
* Sets the minimum content height of this component to the value given.
*
* @param height
*/
public void setMinimumContentHeight(final int height) {
_minimumContentsSize.setHeight(height);
validateContentSize();
}
public boolean isConsumeKeyEvents() {
return _consumeKeyEvents;
}
public void setConsumeKeyEvents(final boolean consume) {
_consumeKeyEvents = consume;
}
public boolean isConsumeMouseEvents() {
return _consumeMouseEvents;
}
public void setConsumeMouseEvents(final boolean consume) {
_consumeMouseEvents = consume;
}
/**
* Ensures content size is between min and max.
*/
protected void validateContentSize() {
final int width = MathUtils.clamp(_contentsSize.getWidth(), _minimumContentsSize.getWidth(),
_maximumContentsSize.getWidth());
final int height = MathUtils.clamp(_contentsSize.getHeight(), _minimumContentsSize.getHeight(),
_maximumContentsSize.getHeight());
_contentsSize.set(width, height);
}
/**
* Sets the size of the content area of this component to the current width and height set on _minimumContentsSize
* (if component is set to allow such resizing.)
*/
public void compact() {
setContentSize(_minimumContentsSize.getWidth(), _minimumContentsSize.getHeight());
}
/**
* Attempt to force this component to fit in the given rectangle.
*
* @param width
* @param height
*/
public void fitComponentIn(final int width, final int height) {
final ReadOnlyTransform local = getTransform();
if (local.isIdentity() || local.getMatrix().isIdentity()) {
setLocalComponentSize(width, height);
return;
}
final Vector3 temp = Vector3.fetchTempInstance();
temp.set(1, 0, 0);
local.applyForwardVector(temp);
if (Math.abs(temp.getX()) >= 0.99999) {
setLocalComponentSize(width, height);
} else if (Math.abs(temp.getY()) >= 0.99999) {
setLocalComponentSize(height, width);
} else {
final Rectangle2 rect = getRelativeMinComponentBounds(null);
final float ratio = Math.min((float) width / rect.getWidth(), (float) height / rect.getHeight());
setLocalComponentSize(Math.round(getMinimumLocalComponentWidth() * ratio),
Math.round(getMinimumLocalComponentHeight() * ratio));
}
}
/**
* Override this to perform actual layout.
*/
public void layout() {
// Let our containers know we are sullied...
fireComponentDirty();
}
/**
* @return the sum of the bottom side of this component's margin, border and padding (if they are set).
*/
public int getTotalBottom() {
int bottom = 0;
if (getMargin() != null) {
bottom += getMargin().getBottom();
}
if (getBorder() != null) {
bottom += getBorder().getBottom();
}
if (getPadding() != null) {
bottom += getPadding().getBottom();
}
return bottom;
}
/**
* @return the sum of the top side of this component's margin, border and padding (if they are set).
*/
public int getTotalTop() {
int top = 0;
if (getMargin() != null) {
top += getMargin().getTop();
}
if (getBorder() != null) {
top += getBorder().getTop();
}
if (getPadding() != null) {
top += getPadding().getTop();
}
return top;
}
/**
* @return the sum of the left side of this component's margin, border and padding (if they are set).
*/
public int getTotalLeft() {
int left = 0;
if (getMargin() != null) {
left += getMargin().getLeft();
}
if (getBorder() != null) {
left += getBorder().getLeft();
}
if (getPadding() != null) {
left += getPadding().getLeft();
}
return left;
}
/**
* @return the sum of the right side of this component's margin, border and padding (if they are set).
*/
public int getTotalRight() {
int right = 0;
if (getMargin() != null) {
right += getMargin().getRight();
}
if (getBorder() != null) {
right += getBorder().getRight();
}
if (getPadding() != null) {
right += getPadding().getRight();
}
return right;
}
/**
* @return the current border set on this component, if any.
*/
public UIBorder getBorder() {
return _border;
}
/**
* @param border
* the border we wish this component to use. May be null.
*/
public void setBorder(final UIBorder border) {
_border = border;
}
/**
* @return the current backdrop set on this component, if any.
*/
public UIBackdrop getBackdrop() {
return _backdrop;
}
/**
* @param backdrop
* the backdrop we wish this component to use. May be null.
*/
public void setBackdrop(final UIBackdrop backDrop) {
_backdrop = backDrop;
}
/**
* @return the current margin set on this component, if any.
*/
public Insets getMargin() {
return _margin;
}
/**
* @param margin
* the new margin (a spacing between the component's border and other components) to set on this
* component. Copied into the component and is allowed to be null.
*/
public void setMargin(final Insets margin) {
if (margin == null) {
_margin = null;
} else if (_margin == null) {
_margin = new Insets(margin);
} else {
_margin.set(margin);
}
}
/**
* @return the current margin set on this component, if any.
*/
public Insets getPadding() {
return _padding;
}
/**
* @param padding
* the new padding (a spacing between the component's border and its inner content area) to set on this
* component. Copied into the component and is allowed to be null.
*/
public void setPadding(final Insets padding) {
if (padding == null) {
_padding = null;
} else if (_padding == null) {
_padding = new Insets(padding);
} else {
_padding.set(padding);
}
}
/**
* @return true if our parent is a UIHud.
*/
public boolean isAttachedToHUD() {
return getParent() instanceof UIHud;
}
/**
* @return the first instance of UIComponent found in this Component's UIComponent ancestry that is attached to the
* hud, or null if none are found. Returns "this" component if it is directly attached to the hud.
*/
public UIComponent getTopLevelComponent() {
if (isAttachedToHUD()) {
return this;
}
final Node parent = getParent();
if (parent instanceof UIComponent) {
return ((UIComponent) parent).getTopLevelComponent();
} else {
return null;
}
}
/**
* @return the first instance of UIHud found in this Component's UIComponent ancestry or null if none are found.
*/
public UIHud getHud() {
final Node parent = getParent();
if (parent instanceof UIHud) {
return (UIHud) parent;
} else if (parent instanceof UIComponent) {
return ((UIComponent) parent).getHud();
} else {
return null;
}
}
/**
* Override to provide an action to take when this component or its top level component are attached to a UIHud.
*/
public void attachedToHud() {}
/**
* Override to provide an action to take just before this component or its top level component are removed from a
* UIHud.
*/
public void detachedFromHud() {}
/**
* @return current screen x coordinate of this component's origin (usually its lower left corner.)
*/
public int getHudX() {
return Math.round(getWorldTranslation().getXf());
}
/**
* @return current screen y coordinate of this component's origin (usually its lower left corner.)
*/
public int getHudY() {
return Math.round(getWorldTranslation().getYf());
}
/**
* Sets the screen x,y coordinate of this component's origin (usually its lower left corner.)
*
* @param x
* @param y
*/
public void setHudXY(final int x, final int y) {
final double newX = getHudX() - getTranslation().getX() + x;
final double newY = getHudY() - getTranslation().getY() + y;
setTranslation(newX, newY, getTranslation().getZ());
}
/**
* @param x
* the new screen x coordinate of this component's origin (usually its lower left corner.)
*/
public void setHudX(final int x) {
final ReadOnlyVector3 translation = getTranslation();
setTranslation(getHudX() - translation.getX() + x, translation.getY(), translation.getZ());
}
/**
* @param y
* the new screen y coordinate of this component's origin (usually its lower left corner.)
*/
public void setHudY(final int y) {
final ReadOnlyVector3 translation = getTranslation();
setTranslation(translation.getX(), getHudY() - translation.getY() + y, translation.getZ());
}
/**
* @return a local x translation from the parent component's content area.
*/
public int getLocalX() {
return (int) Math.round(getTranslation().getX());
}
/**
* @return a local y translation from the parent component's content area.
*/
public int getLocalY() {
return (int) Math.round(getTranslation().getY());
}
/**
* Set the x,y translation from the lower left corner of our parent's content area to the origin of this component.
*
* @param x
* @param y
*/
public void setLocalXY(final int x, final int y) {
setTranslation(x, y, getTranslation().getZ());
fireComponentDirty();
}
/**
* Set the x translation from the lower left corner of our parent's content area to the origin of this component.
*
* @param x
*/
public void setLocalX(final int x) {
final ReadOnlyVector3 translation = getTranslation();
setTranslation(x, translation.getY(), translation.getZ());
fireComponentDirty();
}
/**
* Set the y translation from the lower left corner of our parent's content area to the origin of this component.
*
* @param y
*/
public void setLocalY(final int y) {
final ReadOnlyVector3 translation = getTranslation();
setTranslation(translation.getX(), y, translation.getZ());
fireComponentDirty();
}
/**
* @return the currently set foreground color on this component. Does not inherit from ancestors or default, so may
* return null.
*/
public ReadOnlyColorRGBA getLocalForegroundColor() {
return _foregroundColor;
}
/**
* @return the foreground color associated with this component. If none has been set, we will ask our parent
* component and so on. If no component is found in our ancestry with a foreground color, we will use
* {@link #DEFAULT_FOREGROUND_COLOR}
*/
public ReadOnlyColorRGBA getForegroundColor() {
ReadOnlyColorRGBA foreColor = _foregroundColor;
if (foreColor == null) {
if (getParent() != null && getParent() instanceof UIComponent) {
foreColor = ((UIComponent) getParent()).getForegroundColor();
} else {
foreColor = UIComponent.DEFAULT_FOREGROUND_COLOR;
}
}
return foreColor;
}
/**
* @param color
* the foreground color of this component.
*/
public void setForegroundColor(final ReadOnlyColorRGBA color) {
if (color == null) {
_foregroundColor = null;
} else if (_foregroundColor == null) {
_foregroundColor = new ColorRGBA(color);
} else {
_foregroundColor.set(color);
}
fireStyleChanged();
fireComponentDirty();
}
/**
* @return this component's tooltip text. If none has been set, we will ask our parent component and so on. returns
* null if no tooltips are found.
*/
public String getTooltipText() {
if (_tooltipText != null) {
return _tooltipText;
} else if (getParent() instanceof UIComponent) {
return ((UIComponent) getParent()).getTooltipText();
} else {
return null;
}
}
/**
* @param text
* the tooltip text of this component.
*/
public void setTooltipText(final String text) {
_tooltipText = text;
}
/**
* @return the amount of time in ms to wait before showing a tooltip for this component.
*/
public int getTooltipPopTime() {
return _tooltipPopTime;
}
/**
* @param ms
* the amount of time in ms to wait before showing a tooltip for this component. This is only granular to
* a tenth of a second (or 100ms)
*/
public void setTooltipPopTime(final int ms) {
_tooltipPopTime = ms;
}
/**
* Check if our tooltip timer is active and cancel it.
*/
protected void cancelTooltipTimer() {
if (_showTask != null && !_showTask.isDone()) {
_showTask.cancel(true);
_showTask = null;
}
}
/**
* @param hudX
* the x screen coordinate
* @param hudY
* the y screen coordinate
* @return true if the given screen coordinates fall inside the margin area of this component (or in other words, is
* at the border level or deeper.)
*/
public boolean insideMargin(final int hudX, final int hudY) {
final Vector3 vec = Vector3.fetchTempInstance();
vec.set(hudX, hudY, 0);
getWorldTransform().applyInverse(vec);
final double x = vec.getX() - getMargin().getLeft();
final double y = vec.getY() - getMargin().getBottom();
Vector3.releaseTempInstance(vec);
return x >= 0 && x < getLocalComponentWidth() - getMargin().getLeft() - getMargin().getRight() && y >= 0
&& y < getLocalComponentHeight() - getMargin().getBottom() - getMargin().getTop();
}
/**
* @param hudX
* the x screen coordinate
* @param hudY
* the y screen coordinate
* @return this component (or an appropriate child coordinate in the case of a container) if the given screen
* coordinates fall inside the margin area of this component.
*/
public UIComponent getUIComponent(final int hudX, final int hudY) {
if (getSceneHints().isPickingHintEnabled(PickingHint.Pickable) && isVisible() && insideMargin(hudX, hudY)) {
return this;
}
return null;
}
@Override
public void updateWorldTransform(final boolean recurse) {
updateWorldTransform(recurse, true);
}
/**
* Allow skipping updating our own world transform.
*
* @param recurse
* @param self
*/
protected void updateWorldTransform(final boolean recurse, final boolean self) {
if (self) {
if (_parent != null) {
if (_parent instanceof UIComponent) {
final UIComponent gPar = (UIComponent) _parent;
// grab our parent's world transform
final Transform t = Transform.fetchTempInstance();
t.set(_parent.getWorldTransform());
// shift our origin by total left/bottom
final Vector3 v = Vector3.fetchTempInstance();
v.set(gPar.getTotalLeft(), gPar.getTotalBottom(), 0);
t.applyForwardVector(v);
t.translate(v);
Vector3.releaseTempInstance(v);
// apply our local transform
t.multiply(_localTransform, _worldTransform);
Transform.releaseTempInstance(t);
} else {
_parent.getWorldTransform().multiply(_localTransform, _worldTransform);
}
} else {
_worldTransform.set(_localTransform);
}
}
if (recurse) {
for (int i = getNumberOfChildren() - 1; i >= 0; i--) {
getChild(i).updateWorldTransform(true);
}
}
clearDirty(DirtyType.Transform);
}
@Override
public void draw(final Renderer r) {
// Don't draw if we are not visible.
if (!isVisible()) {
return;
}
boolean clearAlpha = false;
// If we are drawing this component as part of cached container contents, we need to alter the blending to get a
// texture with the correct color and alpha.
if (UIContainer.isDrawingStandin()) {
if (getParent() instanceof UIComponent && !((UIComponent) getParent()).hasVirginContentArea()) {
// we are drawing a sub component onto a surface that already has color, so do a alpha based color blend
// and use the max alpha value.
ContextManager.getCurrentContext().enforceState(UIComponent._blendRGBmaxAlphaBlend);
} else {
// we are drawing a top level component onto an empty texture surface, so use the source color modulated
// by the source alpha and the source alpha value.
ContextManager.getCurrentContext().enforceState(UIComponent._srcRGBmaxAlphaBlend);
}
clearAlpha = true;
}
// Call any predraw operation
predrawComponent(r);
// Draw the component backdrop
if (getBackdrop() != null) {
_virginContentArea = false;
getBackdrop().draw(r, this);
} else {
_virginContentArea = true;
}
// draw the component border
if (getBorder() != null) {
getBorder().draw(r, this);
}
// draw the component, generally editing the content area.
drawComponent(r);
// Call any postdraw operation
postdrawComponent(r);
// Clear enforced blend, if set.
if (clearAlpha) {
ContextManager.getCurrentContext().clearEnforcedState(StateType.Blend);
}
}
/**
* @param defaultFont
* the Font to use as "default" across all UI components that do not have one set.
*/
public static void setDefaultFontFamily(final String family) {
UIComponent._defaultFontFamily = family;
}
/**
* @return the default Font family. Defaults to "Vera".
*/
public static String getDefaultFontFamily() {
return UIComponent._defaultFontFamily;
}
/**
* @param size
* the Font size to use as "default" across all UI components that do not have one set.
*/
public static void setDefaultFontSize(final int size) {
UIComponent._defaultFontSize = size;
}
/**
* @return the default Font size (height). Defaults to 18.
*/
public static int getDefaultFontSize() {
return UIComponent._defaultFontSize;
}
/**
* @param defaultStyles
* the Font styles to use as "default" across all UI components that do not have one set.
*/
public static void setDefaultFontStyles(final Map<String, Object> defaultStyles) {
if (defaultStyles == null) {
UIComponent._defaultFontStyles = Maps.newHashMap();
} else {
UIComponent._defaultFontStyles = Maps.newHashMap(defaultStyles);
}
}
/**
* @return the default Font styles.
*/
public static Map<String, Object> getDefaultFontStyles() {
return UIComponent._defaultFontStyles;
}
/**
* @return a blend state that does alpha blending and writes the max alpha value (source or destination) back to the
* color buffer.
*/
private static BlendState createSrcRGBMaxAlphaBlend() {
final BlendState state = new BlendState();
state.setBlendEnabled(true);
state.setSourceFunctionRGB(SourceFunction.SourceAlpha);
state.setDestinationFunctionRGB(DestinationFunction.Zero);
state.setBlendEquationRGB(BlendEquation.Add);
state.setSourceFunctionAlpha(SourceFunction.SourceAlpha);
state.setDestinationFunctionAlpha(DestinationFunction.DestinationAlpha);
state.setBlendEquationAlpha(BlendEquation.Max);
return state;
}
/**
* @return a blend state that does alpha blending and writes the max alpha value (source or destination) back to the
* color buffer.
*/
private static BlendState createBlendRGBMaxAlphaBlend() {
final BlendState state = new BlendState();
state.setBlendEnabled(true);
state.setSourceFunctionRGB(SourceFunction.SourceAlpha);
state.setDestinationFunctionRGB(DestinationFunction.OneMinusSourceAlpha);
state.setBlendEquationRGB(BlendEquation.Add);
state.setSourceFunctionAlpha(SourceFunction.SourceAlpha);
state.setDestinationFunctionAlpha(DestinationFunction.DestinationAlpha);
state.setBlendEquationAlpha(BlendEquation.Max);
return state;
}
/**
* @return the opacity level set on this component in [0,1], where 0 means completely transparent and 1 is
* completely opaque. If useTransparency is false, this will always return 1.
*/
public float getCombinedOpacity() {
if (UIComponent._useTransparency) {
if (getParent() instanceof UIComponent) {
return _opacity * ((UIComponent) getParent()).getCombinedOpacity();
} else {
return _opacity;
}
} else {
return 1.0f;
}
}
/**
* @return the opacity set on this component directly, not accounting for parent opacity levels.
*/
public float getLocalOpacity() {
return _opacity;
}
/**
* Set the opacity level of this component.
*
* @param opacity
* value in [0,1], where 0 means completely transparent and 1 is completely opaque.
*/
public void setOpacity(final float opacity) {
_opacity = opacity;
}
/**
* Tell all ancestors that use standins, if any, that they need to update any cached graphical representation.
*/
public void fireComponentDirty() {
if (getParent() instanceof UIComponent) {
((UIComponent) getParent()).fireComponentDirty();
}
}
/**
* Let subcomponents know that style has been changed.
*/
public void fireStyleChanged() {}
/**
* @return true if all components should use their opacity value to blend against other components (and/or the 3d
* background scene.)
*/
public static boolean isUseTransparency() {
return UIComponent._useTransparency;
}
/**
* @param useTransparency
* true if all components should use their opacity value to blend against the 3d scene (and each other)
*/
public static void setUseTransparency(final boolean useTransparency) {
UIComponent._useTransparency = useTransparency;
}
/**
* @return the currently rendering component's opacity level. Used by the renderer to alter alpha values based of
* component elements.
*/
public static float getCurrentOpacity() {
return UIComponent._currentOpacity;
}
/**
* Ask this component to update its minimum allowed size, based on its contents.
*/
protected void updateMinimumSizeFromContents() {}
/**
* Perform any pre-draw operations on this component.
*
* @param renderer
*/
protected void predrawComponent(final Renderer renderer) {
UIComponent._currentOpacity = getCombinedOpacity();
}
/**
* Perform any post-draw operations on this component.
*
* @param renderer
*/
protected void postdrawComponent(final Renderer renderer) {}
/**
* Draw this component's contents using the given renderer.
*
* @param renderer
*/
protected void drawComponent(final Renderer renderer) {}
// *******************
// ** INPUT methods
// *******************
/**
* Called when a mouse cursor enters this component.
*
* @param mouseX
* mouse x coordinate.
* @param mouseY
* mouse y coordinate.
* @param state
* the current tracked state of the input system.
*/
public void mouseEntered(final int mouseX, final int mouseY, final InputState state) {
scheduleToolTip();
}
/**
*
*/
private void scheduleToolTip() {
final UIHud hud = getHud();
if (hud != null && getTooltipText() != null) {
final Callable<Void> show = new Callable<Void>() {
public Void call() throws Exception {
while (true) {
if (System.currentTimeMillis() >= _toolDone) {
break;
}
Thread.sleep(100);
}
final UITooltip ttip = hud.getTooltip();
// set contents and size
ttip.getLabel().setText(getTooltipText());
ttip.updateMinimumSizeFromContents();
ttip.getLabel().compact();
ttip.compact();
ttip.layout();
// set position based on CURRENT mouse location.
int x = hud.getLastMouseX();
int y = hud.getLastMouseY();
// Try to keep tooltip on screen.
if (Camera.getCurrentCamera() != null) {
final int displayWidth = Camera.getCurrentCamera().getWidth();
final int displayHeight = Camera.getCurrentCamera().getHeight();
if (x < 0) {
x = 0;
} else if (x + ttip.getLocalComponentWidth() > displayWidth) {
x = displayWidth - ttip.getLocalComponentWidth();
}
if (y < 0) {
y = 0;
} else if (y + ttip.getLocalComponentHeight() > displayHeight) {
y = displayHeight - ttip.getLocalComponentHeight();
}
}
ttip.setHudXY(x, y);
// fire off that we're dirty
ttip.fireComponentDirty();
ttip.updateGeometricState(0, true);
// show
ttip.setVisible(true);
return null;
}
};
cancelTooltipTimer();
resetToolTipTime();
_showTask = new FutureTask<Void>(show);
final Thread t = new Thread() {
@Override
public void run() {
if (_showTask != null && !_showTask.isDone()) {
_showTask.run();
}
}
};
t.setDaemon(true);
t.start();
}
}
/**
* Called when a mouse cursor leaves this component.
*
* @param mouseX
* mouse x coordinate.
* @param mouseY
* mouse y coordinate.
* @param state
* the current tracked state of the input system.
*/
public void mouseDeparted(final int mouseX, final int mouseY, final InputState state) {
cancelTooltipTimer();
final UIHud hud = getHud();
if (hud != null) {
hud.getTooltip().setVisible(false);
}
}
/**
* Called when a mouse button is pressed while the cursor is over this component.
*
* @param button
* the button that was pressed
* @param state
* the current tracked state of the input system.
* @return true if we want to consider the event "consumed" by the UI system.
*/
public boolean mousePressed(final MouseButton button, final InputState state) {
// default is to offer event to parent, if it is a UIComponent
if (!_consumeMouseEvents && getParent() instanceof UIComponent) {
return ((UIComponent) getParent()).mousePressed(button, state);
} else {
return _consumeMouseEvents;
}
}
/**
* Called when a mouse button is released while the cursor is over this component.
*
* @param button
* the button that was released
* @param state
* the current tracked state of the input system.
* @return true if we want to consider the event "consumed" by the UI system.
*/
public boolean mouseReleased(final MouseButton button, final InputState state) {
// default is to offer event to parent, if it is a UIComponent
if (getParent() instanceof UIComponent) {
return ((UIComponent) getParent()).mouseReleased(button, state);
} else {
return _consumeMouseEvents;
}
}
/**
* Called when a mouse button is pressed and released in close proximity while the cursor is over this component.
*
* @param button
* the button that was released
* @param state
* the current tracked state of the input system.
* @return true if we want to consider the event "consumed" by the UI system.
*/
public boolean mouseClicked(final MouseButton button, final InputState state) {
// default is to offer event to parent, if it is a UIComponent
if (getParent() instanceof UIComponent) {
return ((UIComponent) getParent()).mouseClicked(button, state);
} else {
return _consumeMouseEvents;
}
}
/**
* Called when a mouse is moved while the cursor is over this component.
*
* @param mouseX
* mouse x coordinate.
* @param mouseY
* mouse y coordinate.
* @param state
* the current tracked state of the input system.
* @return true if we want to consider the event "consumed" by the UI system.
*/
public boolean mouseMoved(final int mouseX, final int mouseY, final InputState state) {
resetToolTipTime();
// default is to offer event to parent, if it is a UIComponent
if (!_consumeMouseEvents && getParent() instanceof UIComponent) {
return ((UIComponent) getParent()).mouseMoved(mouseX, mouseY, state);
} else {
return _consumeMouseEvents;
}
}
private void resetToolTipTime() {
_toolDone = System.currentTimeMillis() + getTooltipPopTime();
}
/**
* Called when the mouse wheel is moved while the cursor is over this component.
*
* @param wheelDx
* the last change of the wheel
* @param state
* the current tracked state of the input system.
* @return true if we want to consider the event "consumed" by the UI system.
*/
public boolean mouseWheel(final int wheelDx, final InputState state) {
// default is to offer event to parent, if it is a UIComponent
if (!_consumeMouseEvents && getParent() instanceof UIComponent) {
return ((UIComponent) getParent()).mouseWheel(wheelDx, state);
} else {
return _consumeMouseEvents;
}
}
/**
* Called when this component has focus and a key is pressed.
*
* @param key
* the key pressed.
* @param the
* current tracked state of the input system.
* @return true if we want to consider the event "consumed" by the UI system.
*/
public boolean keyPressed(final Key key, final InputState state) {
if (!_consumeKeyEvents && getParent() instanceof UIComponent) {
return ((UIComponent) getParent()).keyPressed(key, state);
} else {
return _consumeKeyEvents;
}
}
/**
* Called when this component has focus and a key is held down over more than 1 input cycle.
*
* @param key
* the key held.
* @param the
* current tracked state of the input system.
* @return true if we want to consider the event "consumed" by the UI system.
*/
public boolean keyHeld(final Key key, final InputState state) {
if (!_consumeKeyEvents && getParent() instanceof UIComponent) {
return ((UIComponent) getParent()).keyHeld(key, state);
} else {
return _consumeKeyEvents;
}
}
/**
* Called when this component has focus and a key is released.
*
* @param key
* the key released.
* @param the
* current tracked state of the input system.
* @return true if we want to consider the event "consumed" by the UI system.
*/
public boolean keyReleased(final Key key, final InputState state) {
if (!_consumeKeyEvents && getParent() instanceof UIComponent) {
return ((UIComponent) getParent()).keyReleased(key, state);
} else {
return _consumeKeyEvents;
}
}
/**
* Called by the hud when a component is given focus.
*/
public void gainedFocus() {}
/**
* Called by the hud when a component loses focus.
*/
public void lostFocus() {}
/**
* Looks up the scenegraph for a Hud and asks it to set us as the currently focused component.
*/
public void requestFocus() {
final UIHud hud = getHud();
if (hud != null) {
hud.setFocusedComponent(this);
}
}
/**
* @return a component we defer to for key focus. Default is null.
*/
public UIComponent getKeyFocusTarget() {
return _keyFocusTarget;
}
/**
* @param target
* a component to set as the actual focused component if this component receives focus.
*/
public void setKeyFocusTarget(final UIComponent target) {
_keyFocusTarget = target;
}
}