////////////////////////////////////////////////////////////////////////////////
//
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You 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 spark.components.supportClasses
{
import flash.display.BitmapData;
import flash.display.DisplayObject;
import flash.display.DisplayObjectContainer;
import flash.events.Event;
import flash.events.KeyboardEvent;
import flash.events.SoftKeyboardEvent;
import flash.events.TextEvent;
import flash.geom.Matrix;
import flash.geom.Matrix3D;
import flash.geom.Point;
import flash.geom.Vector3D;
import flash.text.Font;
import flash.text.FontType;
import flash.text.StyleSheet;
import flash.text.TextField;
import flash.text.TextFieldAutoSize;
import flash.text.TextFieldType;
import flash.text.TextFormat;
import flash.text.TextFormatAlign;
import flash.text.TextInteractionMode;
import flash.text.TextLineMetrics;
import flash.ui.Keyboard;
import flash.utils.Dictionary;
import mx.core.DesignLayer;
import mx.core.FlexGlobals;
import mx.core.FlexTextField;
import mx.core.IInvalidating;
import mx.core.IVisualElement;
import mx.core.UIComponent;
import mx.core.mx_internal;
import mx.events.FlexEvent;
import mx.events.TouchInteractionEvent;
import mx.events.TouchInteractionReason;
import mx.geom.TransformOffsets;
import mx.managers.SystemManager;
import mx.resources.IResourceManager;
import mx.resources.ResourceManager;
import mx.styles.CSSStyleDeclaration;
import mx.styles.ISimpleStyleClient;
import mx.styles.IStyleClient;
import mx.utils.MatrixUtil;
import spark.components.Application;
import spark.components.Scroller;
import spark.core.IEditableText;
import spark.events.TextOperationEvent;
use namespace mx_internal;
//--------------------------------------
// Styles
//--------------------------------------
include "../../styles/metadata/StyleableTextFieldTextStyles.as"
[ResourceBundle("core")]
//
// To use:
// in createChildren():
// var tf:StyleableTextField = createInFontContext(StyleableTextField);
// tf.styleName = this;
// tf.editable = true|false; // for editable text
// tf.multiline = true|false; // for multiline text
// tf.wordWrap = true|false; // for word wrapping
// tf.cacheAsBitmap = true|false; // use true if text in item renderer
// addChild(tf);
//
// in commitProperties():
// tf.text = "...." - if needed
//
// in measure();
// if (tf.isTruncated) // if text may be truncated
// tf.text = "...";
// tf.commitStyles(); // Always call this. No-op if styles already applied.
// Use getElementPreferredWidth(tf), getElementPreferredHeight(tf)
//
// in updateDisplayList():
// if (tf.isTruncated) // if text may be truncated
// tf.text = "...";
// tf.commitStyles(); // Always call this. No-op if styles already applied.
// setElementSize(tf, width, height);
// setElementPosition(x, y);
// // if you want truncated text:
// tf.truncateToFit();
//
// Supported styles: textAlign, fontFamily, fontWeight, "colorName", fontSize, fontStyle,
// textDecoration, textIndent, leading, letterSpacing
/**
* The StyleableTextField class is a text primitive for use in ActionScript
* skins and item renderers. It cannot be used in MXML markup and is not
* compatible with effects.
*
* @langversion 3.0
* @playerversion AIR 2.5
* @productversion Flex 4.5
*/
public class StyleableTextField extends FlexTextField
implements IEditableText, ISimpleStyleClient, IVisualElement
{
//--------------------------------------------------------------------------
//
// Class variables
//
//--------------------------------------------------------------------------
/**
* @private
* Most resources are fetched on the fly from the ResourceManager,
* so they automatically get the right resource when the locale changes.
* But since truncateToFit() can be called frequently,
* this class caches this resource value in this variable.
* Note that this class does _not_ support runtime local changes to
* the truncation indicator. The dynamic local change code in UITextField
* can be used here, if needed.
*/
private static var truncationIndicatorResource:String;
//--------------------------------------------------------------------------
//
// Constructor
//
//--------------------------------------------------------------------------
/**
* Constructor.
*
* @langversion 3.0
* @playerversion AIR 2.5
* @productversion Flex 4.5
*/
public function StyleableTextField()
{
super();
// RichEditableText is double-clickable by default, so we will be too.
doubleClickEnabled = true;
// make our width 400 by default. this is just a heuristic, but it helps
// get the right measurement the first time for a multi-line TextField.
// The developer should still be setting the textField's width to
// the estimatedWidth to get a more accurate representation, but
// sometimes there is no estimated width, and this will be a good
// heuristic in cases where they forget to do that.
width = 400;
// Add a high priority change handler so we can capture the event
// and re-dispatch as a TextOperationEvent
addEventListener(Event.CHANGE, changeHandler, false, 100);
// Add a textInput handler so we can capture and re-dispatch as
// a TextOperationEvent "changing" event.
addEventListener(TextEvent.TEXT_INPUT, textInputHandler);
// Add a key down listener to listen for enter key
addEventListener(KeyboardEvent.KEY_DOWN, keyDownHandler);
addEventListener(TouchInteractionEvent.TOUCH_INTERACTION_STARTING,
touchInteractionStartingHandler);
addEventListener(TouchInteractionEvent.TOUCH_INTERACTION_START,
touchInteractionStartHandler);
addEventListener(TouchInteractionEvent.TOUCH_INTERACTION_END,
touchInteractionEndHandler);
if (!truncationIndicatorResource)
{
truncationIndicatorResource = ResourceManager.getInstance().
getString("core", "truncationIndicator");
}
}
//--------------------------------------------------------------------------
//
// Properties
//
//--------------------------------------------------------------------------
/**
* @private
*/
override public function set width(value:Number):void
{
super.width = value;
// If we're multiline we need to invalidate our size since our height
// may have changed
if (multiline)
{
invalidateTightTextHeight = true;
invalidateTextSizeFlag = true;
}
}
/**
*
* Name of the style to use for determining the text color
*/
private var _colorName:String = "color";
mx_internal function set colorName(value:String):void
{
if (_colorName == value)
return;
_colorName = value;
invalidateStyleFlag = true;
}
/**
* @private
*/
mx_internal function get colorName():String
{
return _colorName;
}
/**
* @private
* Used in place of TextField#textWidth and TextField#textHeight to
* provide consistent size values when accounting for player scaling
* and presence on the stage. Accounts for textIndent and TextField
* gutters.
*/
mx_internal function get measuredTextSize():Point
{
// commit style to get an accurate measurement
commitStyles();
if (!_measuredTextSize)
{
_measuredTextSize = new Point();
invalidateTextSizeFlag = true;
invalidateTightTextHeight = true;
}
if (invalidateTextSizeFlag)
{
var textScaleX:Number = 1;
var textScaleY:Number = 1;
// concatenatedMatrix is not valid when off the stage
if (!stage)
{
var application:Application = (FlexGlobals.topLevelApplication as Application);
var sm:SystemManager = (application) ? (application.systemManager as SystemManager) : null;
if (sm)
{
textScaleX = sm.densityScale;
textScaleY = sm.densityScale;
}
}
else
{
MatrixUtil.decomposeMatrix(decomposition, transform.concatenatedMatrix, 0, 0);
textScaleX = decomposition[3]
textScaleY = decomposition[4]
}
// short circuit measurement when not scaling
if (embedFonts || (textScaleX == 1 && textScaleY == 1))
{
_measuredTextSize.x = textWidth + TEXT_WIDTH_PADDING
_measuredTextSize.y = textHeight + TEXT_HEIGHT_PADDING;
// add textIndent for single line only
if (!multiline)
_measuredTextSize.x += getStyle("textIndent");
}
else
{
// for consistent, scaled measurement:
// If on the stage, use width and height
// If not on the stage, scale the TextField then inverse scale
// the width and height
// Use TextFieldAutoSize.LEFT to compact the text field.
// Using autoSize will change the current size.
// Must save, then restore current state.
var oldWidth:Number = width;
var oldHeight:Number = height;
var oldAutoSize:String = autoSize;
// right and bottom edges close-in on text content
autoSize = TextFieldAutoSize.LEFT;
// apply application scale factor while off stage
if (!stage)
{
var oldScaleX:Number = this.scaleX;
var oldScaleY:Number = this.scaleY;
this.scaleX *= textScaleX;
this.scaleY *= textScaleY;
// apply inverse scaling on the on the scaled TextField size
// this accounts for font scaling behavior in the player
_measuredTextSize.x = width / textScaleX;
_measuredTextSize.y = height / textScaleY;
// remove application scale factor
this.scaleX = oldScaleX;
this.scaleY = oldScaleY;
}
else
{
_measuredTextSize.x = width;
_measuredTextSize.y = height;
}
// restore previous size
autoSize = oldAutoSize;
if (autoSize == TextFieldAutoSize.NONE)
{
super.width = oldWidth;
super.height = oldHeight;
}
}
// Multi-line text enables internal scrolling if we use textHeight. Adding
// leading solves that problem.
if (numLines > 1)
_measuredTextSize.y += getStyle("leading");
// account for floating point errors to fix accidental clipping
// or truncation
_measuredTextSize.x = Math.ceil(_measuredTextSize.x);
_measuredTextSize.y = Math.ceil(_measuredTextSize.y);
invalidateTextSizeFlag = false;
}
return _measuredTextSize;
}
//----------------------------------
// styleDeclaration
//----------------------------------
/**
* @private
* Storage for the styleDeclaration property.
*/
private var _styleDeclaration:CSSStyleDeclaration;
[Inspectable(environment="none")]
/**
* Storage for the inline inheriting styles on this object.
* This CSSStyleDeclaration is created the first time that
* the setStyle()
method
* is called on this component to set an inheriting style.
* Developers typically never need to access this property directly.
*
* @langversion 3.0
* @playerversion AIR 2.5
* @productversion Flex 4.5
*/
public function get styleDeclaration():CSSStyleDeclaration
{
return _styleDeclaration;
}
/**
* @private
*/
public function set styleDeclaration(value:CSSStyleDeclaration):void
{
_styleDeclaration = value;
}
//----------------------------------
// styleName
//----------------------------------
/**
* @private
* Storage for the styleName property.
*/
private var _styleName:Object /* UIComponent */;
/**
* The class style used by this component. This should be an IStyleClient.
*
* @langversion 3.0
* @playerversion AIR 2.5
* @productversion Flex 4.5
*/
public function get styleName():Object /* UIComponent */
{
if (_styleName)
return _styleName;
if (parent is IStyleClient)
return parent;
return null;
}
/**
* @private
*/
public function set styleName(value:Object /* UIComponent */):void
{
if (_styleName === value)
return;
_styleName = value;
styleChanged("styleName");
}
//----------------------------------
// Text alignment helpers
//----------------------------------
/**
* @private
* The height of the first line of text to the baseline of the bottom line
* of text. Handles both single line and multiline text.
*/
private function get tightTextTopOffset():Number
{
updateTightTextSizes();
return _tightTextTopOffset;
}
/**
* @private
* The height of the first line of text to the baseline of the bottom line
* of text. Handles both single line and multiline text.
*/
private function get tightTextHeight() :Number
{
updateTightTextSizes();
return _tightTextHeight;
}
private function updateTightTextSizes():void
{
commitStyles();
// figure out distance from text bottom to last baseline
if (invalidateTightTextHeight)
{
// getLineMetrics() returns strange numbers for an empty string,
// so instead we get the metrics for a non-empty string.
var isEmpty:Boolean = (text == "");
if (isEmpty)
text = "Wj";
var metrics:TextLineMetrics = getLineMetrics(0);
// bottom gutter and descent
var bottomOffset:Number = StyleableTextField.TEXT_HEIGHT_PADDING/2 + metrics.descent;
if (numLines == 1) // account for the extra leading on single line text
bottomOffset += metrics.leading;
_tightTextTopOffset = getTextTopOffset(defaultTextFormat, styleSheet);
_tightTextHeight = measuredTextSize.y - _tightTextTopOffset - bottomOffset;
if (isEmpty)
text = "";
invalidateTightTextHeight = false;
}
}
/**
* @private
* Finds the distance from the top edge of the containing text field to the text for
* a particular font, size, weight and style combination. This value accounts for
* the difference between the metrics.ascent and the true top of the text within the
* text field and the top gutter
*
* If a styleSheet is specified it will be used, otherwise the textFormat will be used.
*/
private static function getTextTopOffset(textFormat:TextFormat, styleSheet:StyleSheet=null):Number
{
// Try to find the top offset for the font, size, weight and style in our table.
// We only store offets for unique font, size, weight and style combinations.
// There could be some other factors that affect textFormat...if we find
// more of them, we will have to change the key to take that into account.
var key:String = textFormat.font + "_" + textFormat.size + "_" + textFormat.bold + "_" + textFormat.italic;
var topOffset:Number = textTopOffsetTable[key];
// if we can't find the value in our table let's calculate it
if (isNaN(topOffset))
{
// default offset is top gutter
topOffset = StyleableTextField.TEXT_HEIGHT_PADDING/2;
// create sample text field
var field:TextField = new TextField();
if (styleSheet)
field.styleSheet = styleSheet;
else
field.defaultTextFormat = textFormat;
field.embedFonts = isFontEmbedded(textFormat);
field.textColor = 0x000000; // make sure our text is black so it will show up against white
field.text = "T"; // use "T" as our standard
// Bitmap data requires non-zero size
if ((field.textWidth > 0) && (field.textHeight > 0))
{
field.width = field.textWidth;
field.height = field.textHeight;
// draw the field into a bitmap data - note default bg color of bitmapData is white.
var bitmapData:BitmapData = new BitmapData(field.width, field.height);
bitmapData.draw(field);
// search vertically for the first non white pixel
var col:int = Math.round(bitmapData.width/2);
for (var i:int = 0; i < bitmapData.height; i++)
{
if (bitmapData.getPixel(col, i) != 0xFFFFFF)
break;
}
if (i < bitmapData.height)
topOffset = i;
}
// store the offset value in our look up table
textTopOffsetTable[key] = topOffset;
}
return topOffset;
}
//--------------------------------------------------------------------------
//
// IDisplayText implementation
//
//--------------------------------------------------------------------------
//----------------------------------
// text
//----------------------------------
/**
* The text displayed by this text component.
*
*
The formatting of this text is controlled by CSS styles. * The supported styles depend on the subclass.
* * @default "" * * @langversion 3.0 * @playerversion AIR 2.5 * @productversion Flex 4.5 */ override public function get text():String { return super.text; } override public function set text(value:String):void { // TextField's text property can't be set to null. if (!value) value = ""; super.text = value; _isTruncated = false; invalidateTextSizeFlag = true; invalidateTightTextHeight = true; if (hasEventListener(FlexEvent.VALUE_COMMIT)) dispatchEvent(new FlexEvent(FlexEvent.VALUE_COMMIT)); } //---------------------------------- // $text //---------------------------------- /** * @private * This property allows access to the Player's native implementation * of the 'text' property, which can be useful since components * can override 'text' and thereby hide the native implementation. * Note that this "base property" is final and cannot be overridden, * so you can count on it to reflect what is happening at the player level. */ mx_internal final function get $text():String { return super.text; } mx_internal final function set $text(value:String):void { super.text = value; } //---------------------------------- // isTruncated //---------------------------------- /** * Indicates whether the text has been truncated,true
,
* or not, false
.
*
* @langversion 3.0
* @playerversion AIR 2.5
* @productversion Flex 4.5
*/
public function get isTruncated():Boolean
{
return _isTruncated;
}
//----------------------------------
// minHeight
//----------------------------------
/**
* @copy mx.core.UIComponent#minHeight
*
* @langversion 3.0
* @playerversion AIR 2.5
* @productversion Flex 4.5
*/
public var minHeight:Number;
//----------------------------------
// minWidth
//----------------------------------
/**
* @copy mx.core.UIComponent#minWidth
*
* @langversion 3.0
* @playerversion AIR 2.5
* @productversion Flex 4.5
*/
public var minWidth:Number;
//--------------------------------------------------------------------------
//
// IEditableText implementation
//
//--------------------------------------------------------------------------
//----------------------------------
// editable
//----------------------------------
/**
* Specifies whether the text is editable, true
,
* or not, false
.
*
* @default true if type is TextFieldType.INPUT, otherwise false.
*
* @langversion 3.0
* @playerversion AIR 2.5
* @productversion Flex 4.5
*/
public function get editable():Boolean
{
return type == TextFieldType.INPUT;
}
public function set editable(value:Boolean):void
{
if (value == editable)
return;
type = value ? TextFieldType.INPUT : TextFieldType.DYNAMIC;
// changing editability, changes size
invalidateTightTextHeight = true;
invalidateTextSizeFlag = true;
dispatchEvent(new Event("editableChanged"));
}
//----------------------------------
// focusEnabled
//----------------------------------
private var _focusEnabled:Boolean = true;
/**
* Indicates whether the component can receive focus when tabbed to.
* You can set focusEnabled
to false
when a
* component is used as a subcomponent of another component so that
* the outer component becomes the focusable entity.
* If this property is false
, focus is transferred to
* the first parent that has focusEnable
set to true
.
*
* @default true
*
* @langversion 3.0
* @playerversion AIR 2.5
* @productversion Flex 4.5
*/
public function get focusEnabled():Boolean
{
return _focusEnabled;
}
public function set focusEnabled(value:Boolean):void
{
_focusEnabled = value;
}
//----------------------------------
// enabled
//----------------------------------
private var _enabled:Boolean = true;
/**
* Whether the component can accept user interaction.
* After setting the enabled
property to false
,
* some components still respond to mouse interactions such as mouseOver
.
* As a result, to fully disable the component, you should also set the value
* of the mouseEnabled
property to false
.
*
* @langversion 3.0
* @playerversion AIR 2.5
* @productversion Flex 4.5
*/
public function get enabled():Boolean
{
return _enabled;
}
public function set enabled(value:Boolean):void
{
_enabled = mouseEnabled = value;
}
//----------------------------------
// horizontalScrollPostion
//----------------------------------
/**
* The horizontal scroll position of the text.
*
* @langversion 3.0
* @playerversion AIR 2.5
* @productversion Flex 4.5
*/
public function get horizontalScrollPosition():Number
{
return scrollH;
}
public function set horizontalScrollPosition(value:Number):void
{
scrollH = Math.min(Math.max(0, int(value)), maxScrollH);
}
//----------------------------------
// lineBreak
//----------------------------------
/**
* Controls word wrapping within the text.
* This property corresponds to the lineBreak
style.
*
* Text may be set to fit the width of the container (LineBreak.TO_FIT
),
* or can be set to break only at explicit return or line feed characters (LineBreak.EXPLICIT
).
Legal values are flashx.textLayout.formats.LineBreak.EXPLICIT
,
* flashx.textLayout.formats.LineBreak.TO_FIT
, and
* flashx.textLayout.formats.FormatValue.INHERIT
.
If a range was selected, the new text replaces the selected text. * If there was an insertion point, the new text is inserted there.
* *An insertion point is then set after the new text. * If necessary, the text will scroll to ensure * that the insertion point is visible.
* * @param text The text to be inserted. * * @throws Error This method or property cannot be used on a text field with a style sheet. * * @langversion 3.0 * @playerversion AIR 2.5 * @productversion Flex 4.5 */ public function insertText(text:String):void { if (styleSheet) { const resourceManager:IResourceManager = ResourceManager.getInstance(); const message:String = resourceManager.getString("components", "styleSheetError"); throw(new Error(message)); } replaceText(selectionAnchorPosition, selectionActivePosition, text); dispatchEvent(new FlexEvent(FlexEvent.VALUE_COMMIT)); invalidateTextSizeFlag = true; invalidateTightTextHeight = true; } /** * Appends the specified text to the end of the text component, * as if you had clicked at the end and typed. * *An insertion point is then set after the new text. * If necessary, the text will scroll to ensure * that the insertion point is visible.
* * @param text The text to be appended. * * @langversion 3.0 * @playerversion AIR 2.5 * @productversion Flex 4.5 */ override public function appendText(text:String):void { super.appendText(text); // Make sure insertion point is at the end of the text var textLength:int = this.text.length; setSelection(textLength, textLength); dispatchEvent(new FlexEvent(FlexEvent.VALUE_COMMIT)); invalidateTextSizeFlag = true; invalidateTightTextHeight = true; } /** * Selects a specified range of characters. * *If either position is negative, it will deselect the text range.
* * @param anchorPosition The character position specifying the end * of the selection that stays fixed when the selection is extended. * * @param activePosition The character position specifying the end * of the selection that moves when the selection is extended. * * @langversion 3.0 * @playerversion AIR 2.5 * @productversion Flex 4.5 */ public function selectRange(anchorIndex:int, activeIndex:int):void { setSelection(Math.min(anchorIndex, activeIndex), Math.max(anchorIndex, activeIndex)); } /** * Selects all of the text. * * @langversion 3.0 * @playerversion AIR 2.5 * @productversion Flex 4.5 */ public function selectAll():void { setSelection(0, length); } /** * Set focus to this text field. * * @langversion 3.0 * @playerversion AIR 2.5 * @productversion Flex 4.5 */ public function setFocus():void { // if we already have the focus, no need to set it again. // if we do indeed set it again, we might end up scrolling the // TextField when we don't want that to happen (SDK-29453) if (stage.focus != this) stage.focus = this; // Work around a runtime bug where calling setSelection(0,0) doesn't // work if the text field is not in focus. // This handles the read-only, non-selectable case if (selectable == false) setSelection(0,0); if (editable) requestSoftKeyboard(); } //-------------------------------------------------------------------------- // // Properties // //-------------------------------------------------------------------------- //-------------------------------------------------------------------------- // // Methods // //-------------------------------------------------------------------------- /** * Commit the styles into the TextField. This method must be called * before the text is displayed, and any time the styles have changed. * This method does nothing if the styles have already been committed. * * @langversion 3.0 * @playerversion AIR 2.5 * @productversion Flex 4.5 */ public function commitStyles():void { if (invalidateStyleFlag) { var align:String = getStyle("textAlign"); if (align == "start") align = TextFormatAlign.LEFT; if (align == "end") align = TextFormatAlign.RIGHT; textFormat.align = align; textFormat.font = getStyle("fontFamily"); textFormat.bold = getStyle("fontWeight") == "bold"; textFormat.color = getStyle(colorName); textFormat.size = getStyle("fontSize"); textFormat.italic = getStyle("fontStyle") == "italic"; textFormat.underline = getStyle("textDecoration") == "underline"; textFormat.indent = getStyle("textIndent"); textFormat.leading = getStyle("leading"); textFormat.letterSpacing = getStyle("letterSpacing"); var kerning:* = getStyle("kerning"); if (kerning == "auto" || kerning == "on") kerning = true; else if (kerning == "default" || kerning == "off") kerning = false; textFormat.kerning = kerning; antiAliasType = getStyle("fontAntiAliasType"); gridFitType = getStyle("fontGridFitType"); sharpness = getStyle("fontSharpness"); thickness = getStyle("fontThickness"); // most components ignore margin and just set x and y, but some (like TextInput) // set the margin increase their hitArea textFormat.leftMargin = leftMargin; textFormat.rightMargin = rightMargin; // Check for embedded fonts embedFonts = isFontEmbedded(textFormat); // It is an error to set defaultTextFormat or call setTextFormat if there is a // styleSheet. if (!styleSheet) { defaultTextFormat = textFormat; setTextFormat(textFormat); } // If our text is empty we need to force the style changes in order for // textHeight to be valid. Setting the width is sufficient, and should // have minimal overhead since we don't have any text. if (text == "") { // Set the width to the fontSize + padding, which is big enough to hold one // character. width = textFormat.size + TEXT_WIDTH_PADDING; } invalidateStyleFlag = false; // now that we've pushed the new styles in, our size might have // changed invalidateTextSizeFlag = true; invalidateBaselinePosition = true; invalidateTightTextHeight = true; } } /** * @copy mx.core.UIComponent#getStyle() * * @langversion 3.0 * @playerversion AIR 2.5 * @productversion Flex 4.5 */ public function getStyle(styleProp:String):* { // check out inline style first if (_inlineStyleObject && _inlineStyleObject[styleProp] !== undefined) return _inlineStyleObject[styleProp]; // check styles that are on us via styleDeclaration if (styleDeclaration && styleDeclaration.getStyle(styleProp) !== undefined) return styleDeclaration.getStyle(styleProp); // if not inlined, check our style provider if (styleName is IStyleClient) return IStyleClient(styleName).getStyle(styleProp); // if can't find it, return undefined return undefined; } /** * @copy mx.core.UIComponent#setStyle() * * @param styleProp Name of the style property. * * @param newValue New value for the style. * * @langversion 3.0 * @playerversion AIR 2.5 * @productversion Flex 4.5 */ public function setStyle(styleProp:String, value:*):void { if (!_inlineStyleObject) _inlineStyleObject = {}; if (value == null) delete _inlineStyleObject[styleProp]; else _inlineStyleObject[styleProp] = value; styleChanged(styleProp); } /** * @copy mx.core.UIComponent#styleChanged() * * @param styleProp The style property that changed. * * @langversion 3.0 * @playerversion AIR 2.5 * @productversion Flex 4.5 */ public function styleChanged(styleProp:String):void { if (styleProp == null || styleProp == "styleName" || supportedStyles.indexOf(styleProp) >= 0 || styleProp == colorName) { invalidateStyleFlag = true; // invalidateSizeFlag doesn't get set until commitStyles() when // the new styles are actually pushed in and the textWidth/textHeight changes } } /** * Truncate text to make it fit horizontally in the area defined for the control, * and append an ellipsis, three periods (...), to the text. This function * only works for single line text. * * @param truncationIndicator The text to be appended after truncation. * If you passnull
, a localizable string
* such as "..."
will be used.
*
* @return true
if the text needed truncation.
*
* @langversion 3.0
* @playerversion AIR 2.5
* @productversion Flex 4.5
*/
public function truncateToFit(truncationIndicator:String = "..."):Boolean
{
if (!truncationIndicator)
truncationIndicator = truncationIndicatorResource;
var originalText:String = super.text;
var oldIsTruncated:Boolean = _isTruncated;
var w:Number = width;
if (leftMargin is Number)
w -= Number(leftMargin);
if (rightMargin is Number)
w -= Number(rightMargin);
_isTruncated = false;
// Need to check if we should truncate, but it
// could be due to rounding error. Let's check that it's not.
// Examples of rounding errors happen with "South Africa" and "Game"
// with verdana.ttf.
if (originalText != "" && measuredTextSize.x > w + 0.00000000000001)
{
// This should get us into the ballpark.
var s:String = originalText;
// TODO (rfrishbe): why is this here below...it does nothing (see SDK-26438)
//originalText.slice(0,
// Math.floor((w / (textWidth + TEXT_WIDTH_PADDING)) * originalText.length));
while (s.length > 1 && (measuredTextSize.x > w))
{
s = s.slice(0, -1);
super.text = s + truncationIndicator;
invalidateTextSizeFlag = true;
}
_isTruncated = true;
invalidateBaselinePosition = true;
invalidateTightTextHeight = true;
// Make sure all text is visible
scrollH = 0;
}
// Dispatch "isTruncatedChange"
if (_isTruncated != oldIsTruncated)
{
if (hasEventListener("isTruncatedChanged"))
dispatchEvent(new Event("isTruncatedChanged"));
}
return _isTruncated;
}
/**
* @private
*/
private static function isFontEmbedded(format:TextFormat):Boolean
{
if (!embeddedFonts)
embeddedFonts = Font.enumerateFonts();
for (var i:int = 0; i < embeddedFonts.length; i++)
{
var font:Font = Font(embeddedFonts[i]);
// embedAsCFF is not supported for StyleableTextField
// reporting CFF as not embedded degrades to default device font
if (font.fontName == format.font &&
(font.fontType != FontType.EMBEDDED_CFF))
{
var style:String = "regular";
if (format.bold && format.italic)
style = "boldItalic";
else if (format.bold)
style = "bold";
else if (format.italic)
style = "italic";
if (font.fontStyle == style)
return true;
}
}
return false;
}
/**
* @private
*/
private function changeHandler(event:Event):void
{
if (!(event is TextOperationEvent))
{
invalidateTextSizeFlag = true;
invalidateTightTextHeight = true;
var newEvent:TextOperationEvent = new TextOperationEvent(event.type);
// stop immediate propagation of the old event
event.stopImmediatePropagation();
// dispatch the new event
dispatchEvent(newEvent);
}
}
/**
* @private
*/
private function keyDownHandler(event:KeyboardEvent):void
{
if (event.isDefaultPrevented())
return;
// Dispatch an "enter" event
if (event.keyCode == Keyboard.ENTER)
{
dispatchEvent(new FlexEvent(FlexEvent.ENTER));
}
}
/**
* @private
*/
private function textInputHandler(event:TextEvent):void
{
// Dispatch a "changing" event
var e:TextOperationEvent = new TextOperationEvent(TextOperationEvent.CHANGING);
var operation:TextInputOperation = new TextInputOperation();
operation.text = event.text;
e.operation = operation;
if (!dispatchEvent(e))
event.preventDefault();
}
/**
* @private
*/
private function touchInteractionStartingHandler(event:TouchInteractionEvent):void
{
// When in text selection mode, tell the Scroller to use the special text
// selection auto-scroll mode, and pass the top/bottom of this component as
// the scroll range.
if (textInteractionMode == TextInteractionMode.SELECTION)
{
var focusThickness:Number = getStyle("focusThickness");
var scroller:Scroller = event.relatedObject as Scroller;
// Early exit if the event isn't a SCROLL event or doesn't have a Scroller
if (event.reason != TouchInteractionReason.SCROLL || !scroller)
return;
// if already in text selection mode with another scroller, cancel this scroller
if (scrollerInTextSelectionMode)
{
event.preventDefault();
return;
}
var minVScrollPos:Number;
var maxVScrollPos:Number;
var minHScrollPos:Number;
var maxHScrollPos:Number;
var pt:Point = new Point(0, 0);
// Offset by our position within the skin/component. The scrolling is
// constrained by the component boundaries, not by the boundaries of
// this text field. We don't have a reliable way to determine the
// component boundaries, so we use our x,y position as an estimate.
pt.offset(-x, -y);
// Include the focus thickness in the min/max scroll positions
pt.offset(-focusThickness, -focusThickness);
pt = localToGlobal(pt);
pt = DisplayObject(scroller.viewport).globalToLocal(pt);
minHScrollPos = Math.max(0, pt.x);
minVScrollPos = Math.max(0, pt.y);
pt.x = width;
pt.y = height;
// We can't reliably find our position relative to the bottom of the skin/
// component, so use our top position as an estimate
pt.offset(x, y);
// Include focus thickness
pt.offset(focusThickness, focusThickness);
pt = parent.localToGlobal(pt);
pt = DisplayObject(scroller.viewport).globalToLocal(pt);
maxHScrollPos = pt.x - scroller.width;
maxVScrollPos = pt.y - scroller.height;
scrollerInTextSelectionMode = scroller;
scroller.enableTextSelectionAutoScroll(true, minHScrollPos, maxHScrollPos,
minVScrollPos, maxVScrollPos);
}
}
/**
* @private
*/
private function touchInteractionStartHandler(event:TouchInteractionEvent):void
{
// During a touch scroll we don't want the keyboard to activate. Add a
// "softKeyboardActivating" handler to cancel the event.
addEventListener(SoftKeyboardEvent.SOFT_KEYBOARD_ACTIVATING,
softKeyboardActivatingHandler);
}
/**
* @private
*/
private function touchInteractionEndHandler(event:TouchInteractionEvent):void
{
// Remove the soft keyboard activate cancelling handler.
removeEventListener(SoftKeyboardEvent.SOFT_KEYBOARD_ACTIVATING,
softKeyboardActivatingHandler);
if (textInteractionMode == TextInteractionMode.SELECTION)
{
var scroller:Scroller = event.relatedObject as Scroller;
// Turn off text selection auto-scroll.
scrollerInTextSelectionMode = null;
if (scroller)
scroller.enableTextSelectionAutoScroll(false);
}
}
/**
* @private
*
* This handler is only added during touch scroll events. It prevents
* the onscreen keyboard from activating if a scroll occurred.
*/
private function softKeyboardActivatingHandler(event:SoftKeyboardEvent):void
{
// Cancelling an ACTIVATING event will close the softKeyboard if it is
// currently active on iOS only. Add a check to only cancel the event
// if the softKeyboard is not active. Otherwise, the softKeyboard will
// close if you start a touch scroll from a text component.
var topLevelApp:Application = FlexGlobals.topLevelApplication as Application;
if (!(topLevelApp && topLevelApp.isSoftKeyboardActive))
event.preventDefault();
}
//--------------------------------------------------------------------------
//
// IVisualElement implementation
//
//--------------------------------------------------------------------------
/**
* @private
*/
public function getBoundsXAtSize(width:Number, height:Number, postLayoutTransform:Boolean=true):Number
{
return x;
}
/**
* @private
*/
public function getBoundsYAtSize(width:Number, height:Number, postLayoutTransform:Boolean=true):Number
{
return y;
}
/**
* @private
*/
public function getLayoutBoundsHeight(postLayoutTransform:Boolean=true):Number
{
if (useTightTextBounds)
{
// we want to return the text field height without the top and bottom offsets
// (measuredTextSize.y - tightTextHeight) gives us the sum of top and bottom offsets
return height - (measuredTextSize.y - tightTextHeight);
}
else
{
return height;
}
}
/**
* @private
*/
public function getLayoutBoundsWidth(postLayoutTransform:Boolean=true):Number
{
if (useTightTextBounds)
{
// return the text field width without the left and right gutters
return width - StyleableTextField.TEXT_GUTTER * 2;
}
else
{
return width;
}
}
/**
* @private
*/
public function getLayoutBoundsX(postLayoutTransform:Boolean=true):Number
{
if (useTightTextBounds)
{
// return the x position of the text within the text field. we calculate this value
// using text field's x, offset by the left gutter
return x + StyleableTextField.TEXT_GUTTER;
}
else
{
return x;
}
}
/**
* @private
*/
public function getLayoutBoundsY(postLayoutTransform:Boolean=true):Number
{
if (useTightTextBounds)
{
// return the y position of the text within the text field. we calculate this value
// using text field's y, offset by the text top offset
return y + tightTextTopOffset;
}
else
{
return y;
}
}
/**
* @private
*/
public function getLayoutMatrix():Matrix
{
return transform.matrix;
}
/**
* @private
*/
public function getLayoutMatrix3D():Matrix3D
{
return transform.matrix3D;
}
/**
* @private
*/
public function getMaxBoundsHeight(postLayoutTransform:Boolean=true):Number
{
return UIComponent.DEFAULT_MAX_WIDTH;
}
/**
* @private
*/
public function getMaxBoundsWidth(postLayoutTransform:Boolean=true):Number
{
return UIComponent.DEFAULT_MAX_WIDTH;
}
/**
* @private
*/
public function getMinBoundsHeight(postLayoutTransform:Boolean=true):Number
{
return isNaN(minHeight) ? 0 : minHeight;
}
/**
* @private
*/
public function getMinBoundsWidth(postLayoutTransform:Boolean=true):Number
{
return isNaN(minWidth) ? 0 : minWidth;
}
/**
* @private
*/
public function getPreferredBoundsHeight(postLayoutTransform:Boolean=true):Number
{
if (useTightTextBounds)
{
// The height from the top of the text to the baseline of the
// last line of text. This is the height used for positioning text
// according to its baseline
return tightTextHeight;
}
else
{
return measuredTextSize.y;
}
}
/**
* @private
*/
public function getPreferredBoundsWidth(postLayoutTransform:Boolean=true):Number
{
if (useTightTextBounds)
{
// The measuredTextSize without the left and right gutters
return measuredTextSize.x - StyleableTextField.TEXT_GUTTER * 2;
}
else
{
return measuredTextSize.x;
}
}
/**
* @private
*/
public function invalidateLayoutDirection():void
{
// do nothing
}
/**
* @private
*/
public function setLayoutBoundsPosition(x:Number, y:Number, postLayoutTransform:Boolean=true):void
{
if (useTightTextBounds)
{
// offset the positions by the left gutters and the top offset
this.x = x - StyleableTextField.TEXT_GUTTER;
this.y = y - tightTextTopOffset;
}
else
{
this.x = x;
this.y = y;
}
}
/**
* @private
*/
public function setLayoutBoundsSize(width:Number, height:Number, postLayoutTransform:Boolean=true):void
{
// width
var newWidth:Number = width;
if (isNaN(newWidth))
newWidth = getPreferredBoundsWidth();
// re-add the left and right gutters
if (useTightTextBounds)
newWidth += StyleableTextField.TEXT_WIDTH_PADDING;
this.width = newWidth;
// height
var newHeight:Number = height;
if (isNaN(newHeight))
newHeight = getPreferredBoundsHeight();
if (useTightTextBounds)
{
if (newHeight > 0)
{
var bottomOffset:Number = measuredTextSize.y - tightTextTopOffset - tightTextHeight;
// when clipping, allow gutter to be outside of height
// use 2x gutter (actual gutter is not part of tight text height)
// when newHeight==1, actual visible height==3 (1px + 1x gutter)
if (newHeight < tightTextHeight)
bottomOffset = StyleableTextField.TEXT_HEIGHT_PADDING;
// re-add the top and bottom offsets. (measuredTextSize.y - tightTextHeight) gives us
// the sum of top and bottom offsets
newHeight += (tightTextTopOffset + bottomOffset);
}
}
this.height = newHeight;
}
/**
* @private
*/
public function setLayoutMatrix(value:Matrix, invalidateLayout:Boolean):void
{
// do nothing
}
/**
* @private
*/
public function setLayoutMatrix3D(value:Matrix3D, invalidateLayout:Boolean):void
{
// do nothing
}
/**
* @private
*/
public function transformAround(transformCenter:Vector3D, scale:Vector3D=null, rotation:Vector3D=null, translation:Vector3D=null, postLayoutScale:Vector3D=null, postLayoutRotation:Vector3D=null, postLayoutTranslation:Vector3D=null, invalidateLayout:Boolean=true):void
{
// do nothing
}
//----------------------------------
// layoutDirection
//----------------------------------
/**
* @private
*/
public function get layoutDirection():String
{
return null;
}
/**
* @private
*/
public function set layoutDirection(value:String):void
{
// do nothing
}
//----------------------------------
// baseline
//----------------------------------
/**
* @private
*/
public function get baseline():Object
{
return null;
}
/**
* @private
*/
public function set baseline(value:Object):void
{
// do nothing
}
//----------------------------------
// baselinePosition
//----------------------------------
/**
* @private
*/
public function get baselinePosition():Number
{
commitStyles();
if (invalidateBaselinePosition)
{
if (useTightTextBounds)
{
_baselinePosition = tightTextHeight;
}
else
{
// getLineMetrics() returns strange numbers for an empty string,
// so instead we get the metrics for a non-empty string.
var isEmpty:Boolean = (text == "");
if (isEmpty)
super.text = "Wj";
_baselinePosition = getLineMetrics(0).ascent + (StyleableTextField.TEXT_HEIGHT_PADDING / 2);
if (isEmpty)
super.text = "";
}
invalidateBaselinePosition = false;
}
return _baselinePosition;
}
//----------------------------------
// bottom
//----------------------------------
private var _bottom:Object;
/**
* @private
*/
public function get bottom():Object
{
return _bottom;
}
/**
* @private
*/
public function set bottom(value:Object):void
{
if (_bottom == value)
return;
_bottom = value;
invalidateParentSizeAndDisplayList();
}
//----------------------------------
// hasLayoutMatrix3D
//----------------------------------
/**
* @private
*/
public function get hasLayoutMatrix3D():Boolean
{
return false;
}
//----------------------------------
// horizontalCenter
//----------------------------------
/**
* @private
*/
public function get horizontalCenter():Object
{
return null;
}
/**
* @private
*/
public function set horizontalCenter(value:Object):void
{
// do nothing
}
//----------------------------------
// includeInLayout
//----------------------------------
/**
* @private
*/
public function get includeInLayout():Boolean
{
return true;
}
/**
* @private
*/
public function set includeInLayout(value:Boolean):void
{
// do nothing
}
//----------------------------------
// left
//----------------------------------
private var _left:Object;
/**
* @private
*/
public function get left():Object
{
return _left;
}
/**
* @private
*/
public function set left(value:Object):void
{
if (_left == value)
return;
_left = value;
invalidateParentSizeAndDisplayList();
}
//----------------------------------
// percentHeight
//----------------------------------
/**
* @private
*/
public function get percentHeight():Number
{
return NaN;
}
/**
* @private
*/
public function set percentHeight(value:Number):void
{
// do nothing
}
//----------------------------------
// percentWidth
//----------------------------------
/**
* @private
*/
public function get percentWidth():Number
{
return NaN;
}
/**
* @private
*/
public function set percentWidth(value:Number):void
{
// do nothing
}
//----------------------------------
// right
//----------------------------------
private var _right:Object;
/**
* @private
*/
public function get right():Object
{
return _right;;
}
/**
* @private
*/
public function set right(value:Object):void
{
if (_right == value)
return;
_right = value;
invalidateParentSizeAndDisplayList();
}
//----------------------------------
// top
//----------------------------------
private var _top:Object;
/**
* @private
*/
public function get top():Object
{
return _top;
}
/**
* @private
*/
public function set top(value:Object):void
{
if (_top == value)
return;
_top = value;
invalidateParentSizeAndDisplayList();
}
//----------------------------------
// verticalCenter
//----------------------------------
/**
* @private
*/
public function get verticalCenter():Object
{
return null;
}
/**
* @private
*/
public function set verticalCenter(value:Object):void
{
// do nothing
}
//----------------------------------
// depth
//----------------------------------
/**
* @private
*/
public function get depth():Number
{
return 0;
}
/**
* @private
*/
public function set depth(value:Number):void
{
// do nothing
}
//----------------------------------
// designLayer
//----------------------------------
/**
* @private
*/
public function get designLayer():DesignLayer
{
return null;
}
/**
* @private
*/
public function set designLayer(value:DesignLayer):void
{
// do nothing
}
//----------------------------------
// is3D
//----------------------------------
/**
* @private
*/
public function get is3D():Boolean
{
return false;
}
//----------------------------------
// owner
//----------------------------------
/**
* @private
*/
public function get owner():DisplayObjectContainer
{
return null;
}
/**
* @private
*/
public function set owner(value:DisplayObjectContainer):void
{
// do nothing
}
//----------------------------------
// postLayoutTransformOffsets
//----------------------------------
/**
* @private
*/
public function get postLayoutTransformOffsets():TransformOffsets
{
return null;
}
/**
* @private
*/
public function set postLayoutTransformOffsets(value:TransformOffsets):void
{
// do nothing
}
/**
* Helper method to invalidate parent size and display list if
* this object affects its layout (includeInLayout is true).
*
* @langversion 3.0
* @playerversion AIR 1.5
* @productversion Flex 4
*/
private function invalidateParentSizeAndDisplayList():void
{
// We want to invalidate both the parent size and parent display list.
if (parent && parent is IInvalidating)
{
IInvalidating(parent).invalidateSize();
IInvalidating(parent).invalidateDisplayList();
}
}
//--------------------------------------------------------------------------
//
// Variables
//
//--------------------------------------------------------------------------
/**
* @private
* Storage for the inline styles on this StyleableTextField instance
*
* There's no real need for _inlineStyleObject because we could
* just piggy-back off of styleDeclaration (and create a new
* CSSStyleDeclaration when setStyle() is called, but this is easier
* and there seems to be less overhead with this approach).
*/
private var _inlineStyleObject:Object;
// Whether or not we want to size and position the text field based upon its
// tight text bounds. Use useTightTextBounds == true when you want precise
// text placement.
mx_internal var useTightTextBounds:Boolean = true;
// Delegate function for scrollToRange. If defined, the scrollToRange
// method is delegated to this function.
mx_internal var scrollToRangeDelegate:Function;
// the left and right margin for this StyleableTextField. By default
// it is null, which corresponds to 0.
mx_internal var leftMargin:Object;
mx_internal var rightMargin:Object;
private static var supportedStyles:String = "textAlign fontFamily fontWeight fontStyle color fontSize textDecoration textIndent leading letterSpacing"
private var invalidateStyleFlag:Boolean = true;
private static var textFormat:TextFormat = new TextFormat();
private var _isTruncated:Boolean = false;
private static var embeddedFonts:Array;
/**
* @private
* Table of text top offsets for different fonts, sizes, weights and styles
*/
private static var textTopOffsetTable:Dictionary = new Dictionary();
/**
* @private
* For text measurement when scaling
*/
private static var decomposition:Vector.