blob: 22bfe40c0d617b6d77ce884cf1416fe04034cbe3 [file] [log] [blame]
////////////////////////////////////////////////////////////////////////////////
//
// 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 flashx.textLayout.elements
{
import flash.display.Graphics;
import flash.display.Shape;
import flash.events.EventDispatcher;
import flash.events.IEventDispatcher;
import flash.geom.Rectangle;
import flash.text.engine.ContentElement;
import flash.text.engine.ElementFormat;
import flash.text.engine.FontDescription;
import flash.text.engine.FontMetrics;
import flash.text.engine.TextBaseline;
import flash.text.engine.TextElement;
import flash.text.engine.TextLine;
import flash.text.engine.TextLineValidity;
import flash.text.engine.TextRotation;
import flash.text.engine.TypographicCase;
import flash.utils.Dictionary;
import flashx.textLayout.compose.FlowComposerBase;
import flashx.textLayout.compose.IFlowComposer;
import flashx.textLayout.compose.ISWFContext;
import flashx.textLayout.compose.StandardFlowComposer;
import flashx.textLayout.compose.TextFlowLine;
import flashx.textLayout.debug.Debugging;
import flashx.textLayout.debug.assert;
import flashx.textLayout.events.FlowElementEventDispatcher;
import flashx.textLayout.events.ModelChange;
import flashx.textLayout.formats.BackgroundColor;
import flashx.textLayout.formats.BaselineShift;
import flashx.textLayout.formats.BlockProgression;
import flashx.textLayout.formats.ColorName;
import flashx.textLayout.formats.FormatValue;
import flashx.textLayout.formats.IMEStatus;
import flashx.textLayout.formats.ITextLayoutFormat;
import flashx.textLayout.formats.TLFTypographicCase;
import flashx.textLayout.formats.TextDecoration;
import flashx.textLayout.formats.TextLayoutFormat;
import flashx.textLayout.tlf_internal;
import flashx.textLayout.utils.CharacterUtil;
import flashx.textLayout.utils.LocaleUtil;
use namespace tlf_internal;
/** Base class for FlowElements that appear at the lowest level of the flow hierarchy. FlowLeafElement objects have
* no children and include InlineGraphicElement objects and SpanElement objects.
*
* @playerversion Flash 10
* @playerversion AIR 1.5
* @langversion 3.0
*
* @see InlineGraphicElement
* @see SpanElement
*/
public class FlowLeafElement extends FlowElement
{
/** Holds the content of the leaf @private */
protected var _blockElement:ContentElement;
/** @private
* Holds the text for the leaf element - unless there's a valid blockElement,
* in which case the text is in the rawText field of the blockElement.
*/
protected var _text:String; // holds the text property if the blockElement is null
private var _hasAttachedListeners:Boolean; // true if FTE eventMirror may be in use
/** @private The event dispatcher that acts as an event mirror */
tlf_internal var _eventMirror:FlowElementEventDispatcher = null;
/**
* Base class - invoking new FlowLeafElement() throws an error exception.
*
* @playerversion Flash 10
* @playerversion AIR 1.5
* @langversion 3.0
*
*/
public function FlowLeafElement()
{
_hasAttachedListeners = false;
super();
}
/** @private */
override tlf_internal function createContentElement():void
{
CONFIG::debug { assert(_blockElement != null, "_blockElement not allocated in derived class"); }
if (_computedFormat)
{
_blockElement.elementFormat = computeElementFormat();
CONFIG::debug { Debugging.traceFTEAssign(_blockElement,"elementFormat",_blockElement.elementFormat); }
}
if (parent)
parent.insertBlockElement(this,_blockElement);
}
/** @private */
override tlf_internal function releaseContentElement():void
{
_blockElement = null;
_computedFormat = null;
}
private function blockElementExists():Boolean
{
return _blockElement != null;
}
/** @private */
tlf_internal function getBlockElement():ContentElement
{
if (!_blockElement)
createContentElement();
return _blockElement;
}
/** @private
* Gets the EventDispatcher associated with this FlowElement. Use the functions
* of EventDispatcher such as <code>setEventHandler()</code> and <code>removeEventHandler()</code>
* to capture events that happen over this FlowLeafElement object. The
* event handler that you specify will be called after this FlowElement object does
* the processing it needs to do.
*
* Note that the event dispatcher will only dispatch FlowElementMouseEvent events.
*
* @playerversion Flash 10
* @playerversion AIR 1.5
* @langversion 3.0
*
* @see flash.events.EventDispatcher
* @see flashx.textLayout.events.FlowElementMouseEvent
*/
tlf_internal override function getEventMirror():IEventDispatcher
{
if (!_eventMirror)
_eventMirror = new FlowElementEventDispatcher(this);
return _eventMirror;
}
/** @private
* Checks whether an event dispatcher is attached, and if so, if the event dispatcher
* has any active listeners.
*/
tlf_internal override function hasActiveEventMirror():Boolean
{
return _eventMirror && (_eventMirror._listenerCount != 0);
}
/** @private This is done so that the TextContainerManager can discover EventMirrors in a TextFlow. */
tlf_internal override function appendElementsForDelayedUpdate(tf:TextFlow,changeType:String):void
{
if (changeType == ModelChange.ELEMENT_ADDED)
{
if (this.hasActiveEventMirror())
{
tf.incInteractiveObjectCount();
getParagraph().incInteractiveChildrenCount() ;
}
}
else if (changeType == ModelChange.ELEMENT_REMOVAL)
{
if (this.hasActiveEventMirror())
{
tf.decInteractiveObjectCount();
getParagraph().decInteractiveChildrenCount() ;
}
}
super.appendElementsForDelayedUpdate(tf,changeType);
}
/**
* The text associated with the FlowLeafElement:
* <p><ul>
* <li>The value for SpanElement subclass will be one character less than <code>textLength</code> if this is the last span in a ParagraphELement.</li>
* <li>The value for BreakElement subclass is a U+2028</li>
* <li>The value for TabElement subclass is a tab</li>
* <li>The value for InlineGraphicElement subclass is U+FDEF</li>
* </ul></p>
*
* @playerversion Flash 10
* @playerversion AIR 1.5
* @langversion 3.0
*
* @see flashx.textLayout.elements.SpanElement#replaceText()
*/
public function get text():String
{
return _text;
}
/** @private */
tlf_internal function getElementFormat():ElementFormat
{
if (!_blockElement)
createContentElement();
return _blockElement.elementFormat;
}
/** @private */
tlf_internal override function setParentAndRelativeStart(newParent:FlowGroupElement,newStart:int):void
{
if (newParent == parent)
return;
var hasBlock:Boolean = _blockElement != null;
if (_blockElement && parent && parent.hasBlockElement()) // remove textElement from the parent content
parent.removeBlockElement(this,_blockElement);
if (newParent && !newParent.hasBlockElement() && _blockElement)
newParent.createContentElement();
super.setParentAndRelativeStart(newParent,newStart);
// Update the FTE ContentElement structure. If the parent has FTE elements, then create FTE elements for the leaf node
// if it doesn't already have them, and add them in. If the parent does not have FTE elements, release the leaf's FTE
// elements also so they match.
if (parent)
{
if (parent.hasBlockElement())
{
if (!_blockElement)
createContentElement();
else if (hasBlock) // don't do this if the _blockElement was constructed as side-effect of setParentAndRelativeStart; in that case, it's already attached
parent.insertBlockElement(this,_blockElement);
}
else if (_blockElement)
releaseContentElement();
}
}
/** @private Only used by SpanElement.splitAtPosition */
tlf_internal function quickInitializeForSplit(sibling:FlowLeafElement,newSpanLength:int,newSpanTextElement:TextElement):void
{
setTextLength(newSpanLength);
_blockElement = newSpanTextElement;
if (_blockElement)
_text = _blockElement.text;
quickCloneTextLayoutFormat(sibling);
var tf:TextFlow = sibling.getTextFlow();
if (tf == null || tf.formatResolver == null)
{
_computedFormat = sibling._computedFormat;
if (_blockElement)
_blockElement.elementFormat = sibling.getElementFormat();
}
}
/**
* Returns the next FlowLeafElement object.
*
* @param limitElement Specifies FlowGroupElement on whose last leaf to stop looking. A value of null (default)
* means search till no more elements.
* @return next FlowLeafElement or null if at the end
*
* @includeExample examples\FlowLeafElement_getNextLeafExample.as -noswf
*
* @playerversion Flash 10
* @playerversion AIR 1.5
* @langversion 3.0
*
*/
public function getNextLeaf(limitElement:FlowGroupElement=null):FlowLeafElement
{
if (!parent)
return null;
return parent.getNextLeafHelper(limitElement,this);
}
/**
* Returns the previous FlowLeafElement object.
*
* @param limitElement Specifies the FlowGroupElement on whose first leaf to stop looking. null (default) means search till no more elements.
* @return previous leafElement or null if at the end
*
* @includeExample examples\FlowLeafElement_getPreviousLeafExample.as -noswf
*
* @playerversion Flash 10
* @playerversion AIR 1.5
* @langversion 3.0
*
*/
public function getPreviousLeaf(limitElement:FlowGroupElement=null):FlowLeafElement
{
if (!parent)
return null;
return parent.getPreviousLeafHelper(limitElement,this);
}
/** @private */
public override function getCharAtPosition(relativePosition:int):String
{
return _text ? _text.charAt(relativePosition) : "";
}
/** @private */
tlf_internal override function normalizeRange(normalizeStart:uint,normalizeEnd:uint):void
{
// this does the cascade - potential optimization to skip it if the _blockElement isn't attached
if (_blockElement)
computedFormat;
}
/** Returns the FontMetrics object for the span. The properties of the FontMetrics object describe the
* emBox, strikethrough position, strikethrough thickness, underline position,
* and underline thickness for the specified font.
*
* @playerversion Flash 10
* @playerversion AIR 1.5
* @langversion 3.0
*
* @see flash.text.engine.FontMetrics
* @see flash.text.engine.ElementFormat#getFontMetrics()
*
* @return font metrics associated with the span
*/
public function getComputedFontMetrics():FontMetrics
{
if (!_blockElement)
createContentElement();
var ef:ElementFormat = _blockElement.elementFormat;
if (!ef)
return null;
var tf:TextFlow = getTextFlow()
if (tf && tf.flowComposer && tf.flowComposer.swfContext)
return tf.flowComposer.swfContext.callInContext(ef.getFontMetrics,ef,null,true);
return ef.getFontMetrics();
}
/** @private */
tlf_internal static function resolveDomBaseline(computedFormat:ITextLayoutFormat, para:ParagraphElement):String
{
CONFIG::debug { assert(computedFormat != null,"bad call to resolveDomBaseline"); }
var domBase:String = computedFormat.dominantBaseline;
if(domBase == FormatValue.AUTO)
{
if(computedFormat.textRotation == TextRotation.ROTATE_270 /*||
this.computedFormat.blockProgression == BlockProgression.RL*/)
domBase = TextBaseline.IDEOGRAPHIC_CENTER;
else
{
//otherwise, avoid using the locale of the element and use the paragraph's locale
if(para != null)
domBase = para.getEffectiveDominantBaseline();
else
domBase = LocaleUtil.dominantBaseline(computedFormat.locale);
}
}
return domBase;
}
/** @private */
tlf_internal function computeElementFormat():ElementFormat
{
CONFIG::debug { assert(_computedFormat != null,"bad call to computeElementFormat"); }
var tf:TextFlow = getTextFlow();
return computeElementFormatHelper (_computedFormat, getParagraph(), tf && tf.flowComposer ? tf.flowComposer.swfContext : null);
}
// mjzhang : fix for bug# 2758977 <s:p color="red"/> throws out of range error - can you do color lookup like Flex SDK
/** @private */
static private function translateColor(color:String):Number
{
var ret:Number = NaN;
switch ( color.toLowerCase() )
{
case ColorName.BLACK:
ret = 0x000000; break;
case ColorName.BLUE:
ret = 0x0000FF; break;
case ColorName.GREEN:
ret = 0x008000; break;
case ColorName.GRAY:
ret = 0x808080; break;
case ColorName.SILVER:
ret = 0xC0C0C0; break;
case ColorName.LIME:
ret = 0x00FF00; break;
case ColorName.OLIVE:
ret = 0x808000; break;
case ColorName.WHITE:
ret = 0xFFFFFF; break;
case ColorName.YELLOW:
ret = 0xFFFF00; break;
case ColorName.MAROON:
ret = 0x800000; break;
case ColorName.NAVY:
ret = 0x000080; break;
case ColorName.RED:
ret = 0xFF0000; break;
case ColorName.PURPLE:
ret = 0x800080; break;
case ColorName.TEAL:
ret = 0x008080; break;
case ColorName.FUCHSIA:
ret = 0xFF00FF; break;
case ColorName.AQUA:
ret = 0x00FFFF; break;
case ColorName.MAGENTA:
ret = 0xFF00FF; break;
case ColorName.CYAN:
ret = 0x00FFFF; break;
default : break;
}
return ret;
}
/** @private */
static tlf_internal function computeElementFormatHelper(computedFormat:ITextLayoutFormat, para:ParagraphElement, swfContext:ISWFContext):ElementFormat
{
// compute the cascaded elementFormat
var format:ElementFormat = new ElementFormat();
CONFIG::debug { Debugging.traceFTECall(format,null,"new ElementFormat()"); }
format.alignmentBaseline = computedFormat.alignmentBaseline;
format.alpha = Number(computedFormat.textAlpha);
format.breakOpportunity = computedFormat.breakOpportunity;
format.color = (computedFormat.color is String) ? translateColor(computedFormat.color) : uint(computedFormat.color);
format.dominantBaseline = resolveDomBaseline(computedFormat, para);
format.digitCase = computedFormat.digitCase;
format.digitWidth = computedFormat.digitWidth;
format.ligatureLevel = computedFormat.ligatureLevel;
format.fontSize = Number(computedFormat.fontSize);
format.kerning = computedFormat.kerning;
format.locale = computedFormat.locale;
format.trackingLeft = TextLayoutFormat.trackingLeftProperty.computeActualPropertyValue(computedFormat.trackingLeft,format.fontSize);
format.trackingRight = TextLayoutFormat.trackingRightProperty.computeActualPropertyValue(computedFormat.trackingRight,format.fontSize);
format.textRotation = computedFormat.textRotation;
format.baselineShift = -(TextLayoutFormat.baselineShiftProperty.computeActualPropertyValue(computedFormat.baselineShift, format.fontSize));
switch (computedFormat.typographicCase)
{
case TLFTypographicCase.LOWERCASE_TO_SMALL_CAPS:
format.typographicCase = TypographicCase.CAPS_AND_SMALL_CAPS;
break;
case TLFTypographicCase.CAPS_TO_SMALL_CAPS:
format.typographicCase = TypographicCase.SMALL_CAPS;
break;
/* Others map directly so handle it in the default case */
default:
format.typographicCase = computedFormat.typographicCase;
break;
}
CONFIG::debug { Debugging.traceFTEAssign(format,"alignmentBaseline",format.alignmentBaseline); }
CONFIG::debug { Debugging.traceFTEAssign(format,"alpha",format.alpha); }
CONFIG::debug { Debugging.traceFTEAssign(format,"breakOpportunity",format.breakOpportunity); }
CONFIG::debug { Debugging.traceFTEAssign(format,"color",format.color); }
CONFIG::debug { Debugging.traceFTEAssign(format,"dominantBaseline",format.dominantBaseline); }
CONFIG::debug { Debugging.traceFTEAssign(format,"digitCase",format.digitCase); }
CONFIG::debug { Debugging.traceFTEAssign(format,"digitWidth",format.digitWidth); }
CONFIG::debug { Debugging.traceFTEAssign(format,"ligatureLevel",format.ligatureLevel); }
CONFIG::debug { Debugging.traceFTEAssign(format,"fontSize",format.fontSize); }
CONFIG::debug { Debugging.traceFTEAssign(format,"kerning",format.kerning); }
CONFIG::debug { Debugging.traceFTEAssign(format,"locale",format.locale); }
CONFIG::debug { Debugging.traceFTEAssign(format,"trackingLeft",format.trackingLeft); }
CONFIG::debug { Debugging.traceFTEAssign(format,"trackingRight",format.trackingRight); }
CONFIG::debug { Debugging.traceFTEAssign(format,"typographicCase",format.typographicCase); }
CONFIG::debug { Debugging.traceFTEAssign(format,"textRotation",format.textRotation); }
CONFIG::debug { Debugging.traceFTEAssign(format,"baselineShift",format.baselineShift); }
// set the fontDesription in the cascadedFormat
var fd:FontDescription = new FontDescription();
fd.fontWeight = computedFormat.fontWeight;
fd.fontPosture = computedFormat.fontStyle;
fd.fontName = computedFormat.fontFamily;
fd.renderingMode = computedFormat.renderingMode;
fd.cffHinting = computedFormat.cffHinting;
// the fontLookup may be override by the resolveFontLookupFunction
if (GlobalSettings.resolveFontLookupFunction == null)
fd.fontLookup = computedFormat.fontLookup;
else
{
fd.fontLookup = GlobalSettings.resolveFontLookupFunction(swfContext ? FlowComposerBase.computeBaseSWFContext(swfContext) : null, computedFormat);
}
// and now give the fontMapper a shot at rewriting the FontDescription
var fontMapper:Function = GlobalSettings.fontMapperFunction;
if (fontMapper != null)
fontMapper(fd);
CONFIG::debug { Debugging.traceFTECall(fd,null,"new FontDescription()"); }
CONFIG::debug { Debugging.traceFTEAssign(fd,"fontWeight",fd.fontWeight); }
CONFIG::debug { Debugging.traceFTEAssign(fd,"fontPosture",fd.fontPosture); }
CONFIG::debug { Debugging.traceFTEAssign(fd,"fontName",fd.fontName); }
CONFIG::debug { Debugging.traceFTEAssign(fd,"renderingMode",fd.renderingMode); }
CONFIG::debug { Debugging.traceFTEAssign(fd,"cffHinting",fd.cffHinting); }
CONFIG::debug { Debugging.traceFTEAssign(fd,"fontLookup",fd.fontLookup); }
format.fontDescription = fd;
CONFIG::debug { Debugging.traceFTEAssign(format,"fontDescription",fd); }
//Moved code here because original code tried to access fontMetrics prior to setting the elementFormat.
//Since getFontMetrics returns the value of blockElement.elementFormat.getFontMetrics(), we cannot call this
//until after the element has been set. Watson 1820571 - gak 06.11.08
// Adjust format for superscript/subscript
if (computedFormat.baselineShift == BaselineShift.SUPERSCRIPT ||
computedFormat.baselineShift == BaselineShift.SUBSCRIPT)
{
var fontMetrics:FontMetrics;
if (swfContext)
fontMetrics = swfContext.callInContext(format.getFontMetrics,format,null,true);
else
fontMetrics = format.getFontMetrics();
if (computedFormat.baselineShift == BaselineShift.SUPERSCRIPT)
{
format.baselineShift = (fontMetrics.superscriptOffset * format.fontSize);
format.fontSize = fontMetrics.superscriptScale * format.fontSize;
}
else // it's subscript
{
format.baselineShift = (fontMetrics.subscriptOffset * format.fontSize);
format.fontSize = fontMetrics.subscriptScale * format.fontSize;
}
CONFIG::debug { Debugging.traceFTEAssign(format,"baselineShift",format.baselineShift); }
CONFIG::debug { Debugging.traceFTEAssign(format,"fontSize",format.fontSize); }
}
return format;
}
/**
* The computed text format attributes that are in effect for this element.
* Takes into account the inheritance of attributes.
*
* @playerversion Flash 10
* @playerversion AIR 1.5
* @langversion 3.0
*
* @see flashx.textLayout.formats.ITextLayoutFormat
*/
public override function get computedFormat():ITextLayoutFormat
{
if (!_computedFormat)
{
_computedFormat = doComputeTextLayoutFormat();
if (_blockElement)
{
_blockElement.elementFormat = computeElementFormat();
CONFIG::debug { Debugging.traceFTEAssign(_blockElement,"elementFormat",_blockElement.elementFormat); }
}
}
return _computedFormat;
}
/** Returns the calculated lineHeight from this element's properties. @private */
tlf_internal function getEffectiveLineHeight(blockProgression:String):Number
{
//ignore the leading on a TCY Block. If the element is in a TCYBlock, it has no leading
if (blockProgression == BlockProgression.RL && (parent is TCYElement))
return 0;
CONFIG::debug { assert(_computedFormat != null,"Missing _computedFormat in FlowLeafElement.getEffectiveLineHeight"); }
return TextLayoutFormat.lineHeightProperty.computeActualPropertyValue(computedFormat.lineHeight, getEffectiveFontSize());
}
/** @private
* Get the "inline box" for the element as defined by the CSS visual formatting model (http://www.w3.org/TR/CSS2/visuren.html)
* @param blockProgression Block progression
* @param textLine The containing text line
* @param para The containing para. Only used for resolving AUTO dominantBaseline value.
* May be null, in which case the AUTO dominantBaseline value is resolved based on other attributes (such as the element's computed locale).
* @param swfContext The SWF context in which certain method calls (such as the one used to get font metrics) are made
* May be null in which case the current SWF context is used.
* @return Null if the element is not "inline"
* Otherwise, a rectangle representing the inline box. Top and Bottom are relative to the line's Roman baseline. Left and Right are ignored.
*/
tlf_internal function getCSSInlineBox(blockProgression:String, textLine:TextLine, para:ParagraphElement=null, swfContext:ISWFContext=null):Rectangle
{
// TODO-9/27/10: TCYs typically don't affect leading, but this may not be appropriate for LeadingModel.BOX
if (blockProgression == BlockProgression.RL && (parent is TCYElement))
return null;
return getCSSInlineBoxHelper (computedFormat, getComputedFontMetrics(), textLine, para);
}
/** @private
* Get the "inline box" for an element with the specified computed format as defined by the CSS visual formatting model (http://www.w3.org/TR/CSS2/visuren.html)
* For a span, leading is applied equally above and below the em-box such that the box's height equals lineHeight.
* Alignment relative to the baseline (using baselineShift, dominantBaseline, alignmentBaseline) is taken into account.
* @param textLine The containing text line
* @param para The containing para. Only used for resolving AUTO dominantBaseline value.
* May be null, in which case the AUTO dominantBaseline value is resolved based on other attributes (such as the element's computed locale).
* @return A rectangle representing the inline box. Top and Bottom are relative to the line's Roman baseline. Left and Right are ignored.
*/
static tlf_internal function getCSSInlineBoxHelper(computedFormat:ITextLayoutFormat, metrics:FontMetrics, textLine:TextLine, para:ParagraphElement=null):Rectangle
{
var emBox:Rectangle = metrics.emBox;
var ascent:Number = -emBox.top;
var descent:Number = emBox.bottom;
var textHeight:Number = emBox.height;
var fontSize:Number = computedFormat.fontSize;
var lineHeight:Number = TextLayoutFormat.lineHeightProperty.computeActualPropertyValue(computedFormat.lineHeight, fontSize);
var halfLeading:Number = (lineHeight - textHeight) / 2;
// Apply half leading equally above and below the em box
emBox.top -= halfLeading;
emBox.bottom += halfLeading;
// Account for the effect of dominantBaseline
var computedDominantBaseline:String = resolveDomBaseline(computedFormat, para);
switch (computedDominantBaseline)
{
case TextBaseline.ASCENT:
case TextBaseline.IDEOGRAPHIC_TOP:
emBox.offset(0, ascent);
break;
case TextBaseline.IDEOGRAPHIC_CENTER:
emBox.offset(0, ascent - textHeight/2);
break;
case TextBaseline.ROMAN:
break;
case TextBaseline.DESCENT:
case TextBaseline.IDEOGRAPHIC_BOTTOM:
emBox.offset(0, -descent);
}
// Account for the effect of alignmentBaseline
var computedAlignmentBaseline:String = (computedFormat.alignmentBaseline == TextBaseline.USE_DOMINANT_BASELINE ? computedDominantBaseline : computedFormat.alignmentBaseline);
emBox.offset(0, textLine.getBaselinePosition(computedAlignmentBaseline));
// Account for the effect of baselineShift
var baselineShift:Number;
if (computedFormat.baselineShift == BaselineShift.SUPERSCRIPT)
baselineShift = metrics.superscriptOffset * fontSize;
else if (computedFormat.baselineShift == BaselineShift.SUBSCRIPT)
baselineShift = metrics.subscriptOffset * fontSize;
else
baselineShift = -computedFormat.baselineShift;
emBox.offset(0, baselineShift);
return emBox;
}
/** Returns the fontSize from this element's properties. @private */
tlf_internal function getEffectiveFontSize():Number
{
return Number(computedFormat.fontSize);
}
/** @private */
tlf_internal function getSpanBoundsOnLine(textLine:TextLine, blockProgression:String):Array
{
var line:TextFlowLine = TextFlowLine(textLine.userData);
var paraStart:int = line.paragraph.getAbsoluteStart();
var lineEnd:int = (line.absoluteStart + line.textLength) - paraStart;
var spanStart:int = getAbsoluteStart() - paraStart; // get start pos relative to the paragraph (which might not be the parent)
var endPos:int = spanStart + text.length; // so we don't include the paragraph terminator character, if present
// Clip to start of line
var startPos:int = Math.max(spanStart, line.absoluteStart - paraStart);
// Clip to end of line
// Heuristic for detecting spaces at the end of the line and eliminating them from the range so they won't be underlined.
if (endPos >= lineEnd)
{
endPos = lineEnd;
var spanText:String = text;
while (endPos > startPos && CharacterUtil.isWhitespace(spanText.charCodeAt(endPos - spanStart - 1)))
--endPos;
}
var mainRects:Array = [];
line.calculateSelectionBounds(textLine, mainRects, startPos, endPos, blockProgression, [ line.textHeight,0]);
return mainRects;
}
/** @private */
tlf_internal function updateIMEAdornments(tLine:TextLine, blockProgression:String, imeStatus:String):void
{
var metrics:FontMetrics = getComputedFontMetrics();
var spanBoundsArray:Array = getSpanBoundsOnLine(tLine, blockProgression);
//this is pretty much always going to have a length of 1, but just to be sure...
for (var i:int = 0; i < spanBoundsArray.length; i++)
{
//setup ime variables
var imeLineThickness:int = 1;
var imeLineColor:uint = 0x000000;
var imeLineStartX:Number = 0;
var imeLineStartY:Number = 0;
var imeLineEndX:Number = 0;
var imeLineEndY:Number = 0;
//selected text draws with 2 px
if(imeStatus == IMEStatus.SELECTED_CONVERTED || imeStatus == IMEStatus.SELECTED_RAW)
{
imeLineThickness = 2;
}
//Raw or deadkey text draws with grey
if(imeStatus == IMEStatus.SELECTED_RAW || imeStatus == IMEStatus.NOT_SELECTED_RAW
|| imeStatus == IMEStatus.DEAD_KEY_INPUT_STATE)
{
imeLineColor = 0xA6A6A6;
}
var spanBounds:Rectangle = spanBoundsArray[i] as Rectangle;
var stOffset:Number = calculateStrikeThrough(tLine, blockProgression, metrics);
var ulOffset:Number = calculateUnderlineOffset(stOffset, blockProgression, metrics, tLine);
if (blockProgression != BlockProgression.RL)
{
imeLineStartX = spanBounds.topLeft.x + 1;
imeLineEndX = spanBounds.topLeft.x + spanBounds.width - 1;
imeLineStartY = ulOffset;
imeLineEndY = ulOffset;
}
else
{
//is this TCY?
var line:TextFlowLine = tLine.userData as TextFlowLine;
var elemIdx:int = this.getAbsoluteStart() - line.absoluteStart;
imeLineStartY = spanBounds.topLeft.y + 1;
imeLineEndY = spanBounds.topLeft.y + spanBounds.height - 1;
//elemIdx can sometimes be negative if the text is being wrapped due to a
//resize gesture - in which case the tLine has not necessarily been updated.
//If the elemIdx is invalid, just treat it like it's normal ttb text - gak 07.08.08
if(elemIdx < 0 || tLine.atomCount <= elemIdx || tLine.getAtomTextRotation(elemIdx) != TextRotation.ROTATE_0)
{
imeLineStartX = ulOffset;
imeLineEndX = ulOffset;
}
else
{
//it is TCY!
var tcyParent:TCYElement = this.getParentByType(TCYElement) as TCYElement;
CONFIG::debug{ assert(tcyParent != null, "What kind of object is this that is ROTATE_0, but not TCY?");}
//only perform calculations for TCY adornments when we are on the last leaf. ONLY the last leaf matters
if((this.getAbsoluteStart() + this.textLength) == (tcyParent.getAbsoluteStart() + tcyParent.textLength))
{
var tcyAdornBounds:Rectangle = new Rectangle(); // NO PMD
tcyParent.calculateAdornmentBounds(tcyParent, tLine, blockProgression, tcyAdornBounds);
var baseULAdjustment:Number = metrics.underlineOffset + (metrics.underlineThickness/2);
imeLineStartY = tcyAdornBounds.top + 1;
imeLineEndY = tcyAdornBounds.bottom - 1;
imeLineStartX = spanBounds.bottomRight.x + baseULAdjustment;
imeLineEndX = spanBounds.bottomRight.x + baseULAdjustment;
}
}
}
//Build the shape
var selObj:Shape = new Shape(); // NO PMD
//TODO - this is probably going to need to be overridable in the full implementation
selObj.alpha = 1;
selObj.graphics.beginFill(imeLineColor);
selObj.graphics.lineStyle(imeLineThickness, imeLineColor, selObj.alpha);
selObj.graphics.moveTo(imeLineStartX, imeLineStartY);
selObj.graphics.lineTo(imeLineEndX, imeLineEndY);
selObj.graphics.endFill();
tLine.addChild(selObj);
}
}
/** @private return number of shapes added */
tlf_internal function updateAdornments(tLine:TextLine, blockProgression:String):int
{
CONFIG::debug { assert(_computedFormat != null,"invalid call to updateAdornments"); }
// Only work on lines with strikethrough or underline
if (_computedFormat.textDecoration == TextDecoration.UNDERLINE || _computedFormat.lineThrough || _computedFormat.backgroundAlpha > 0 && _computedFormat.backgroundColor != BackgroundColor.TRANSPARENT)
{
var spanBoundsArray:Array = getSpanBoundsOnLine(tLine, blockProgression);
for (var i:int = 0; i < spanBoundsArray.length; i++)
updateAdornmentsOnBounds(tLine, blockProgression, spanBoundsArray[i]);
return spanBoundsArray.length ;
}
return 0;
}
private function updateAdornmentsOnBounds(tLine:TextLine, blockProgression:String, spanBounds:Rectangle):void
{
CONFIG::debug { assert(_computedFormat != null,"invalid call to updateAdornmentsOnBounds"); }
var metrics:FontMetrics = getComputedFontMetrics();
var backgroundOnly:Boolean = !(_computedFormat.textDecoration == TextDecoration.UNDERLINE || _computedFormat.lineThrough);
if (!backgroundOnly)
{
var shape:Shape;
var g:Graphics;
shape = new Shape();
shape.alpha = Number(_computedFormat.textAlpha);
g = shape.graphics;
var stOffset:Number = calculateStrikeThrough(tLine, blockProgression, metrics);
var ulOffset:Number = calculateUnderlineOffset(stOffset, blockProgression, metrics, tLine);
}
if (blockProgression != BlockProgression.RL)
{
addBackgroundRect (tLine, metrics, spanBounds, true);
if (_computedFormat.textDecoration == TextDecoration.UNDERLINE)
{
g.lineStyle(metrics.underlineThickness, _computedFormat.color as uint);
g.moveTo(spanBounds.topLeft.x, ulOffset);
g.lineTo(spanBounds.topLeft.x + spanBounds.width, ulOffset);
}
if((_computedFormat.lineThrough))
{
g.lineStyle(metrics.strikethroughThickness, _computedFormat.color as uint);
g.moveTo(spanBounds.topLeft.x, stOffset);
g.lineTo(spanBounds.topLeft.x + spanBounds.width, stOffset);
}
}
else
{
//is this TCY?
var line:TextFlowLine = tLine.userData as TextFlowLine;
var elemIdx:int = this.getAbsoluteStart() - line.absoluteStart;
//elemIdx can sometimes be negative if the text is being wrapped due to a
//resize gesture - in which case the tLine has not necessarily been updated.
//If the elemIdx is invalid, just treat it like it's normal ttb text - gak 07.08.08
if (elemIdx < 0 || tLine.atomCount <= elemIdx || tLine.getAtomTextRotation(elemIdx) != TextRotation.ROTATE_0)
{
addBackgroundRect (tLine, metrics, spanBounds, false);
if (_computedFormat.textDecoration == TextDecoration.UNDERLINE)
{
g.lineStyle(metrics.underlineThickness, _computedFormat.color as uint);
g.moveTo(ulOffset, spanBounds.topLeft.y);
g.lineTo(ulOffset, spanBounds.topLeft.y + spanBounds.height);
}
if (_computedFormat.lineThrough == true)
{
g.lineStyle(metrics.strikethroughThickness, _computedFormat.color as uint);
g.moveTo(-stOffset, spanBounds.topLeft.y);
g.lineTo(-stOffset, spanBounds.topLeft.y + spanBounds.height);
}
}
else
{
addBackgroundRect (tLine, metrics, spanBounds, true, true);
if (!backgroundOnly)
{
//it is TCY!
var tcyParent:TCYElement = this.getParentByType(TCYElement) as TCYElement;
CONFIG::debug{ assert(tcyParent != null, "What kind of object is this that is ROTATE_0, but not TCY?");}
//if the locale of the paragraph is Chinese, we need to adorn along the left and not right side.
var tcyPara:ParagraphElement = this.getParentByType(ParagraphElement) as ParagraphElement;
var lowerLocale:String = tcyPara.computedFormat.locale.toLowerCase();
var adornRight:Boolean = (lowerLocale.indexOf("zh") != 0);
//only perform calculations for TCY adornments when we are on the last leaf. ONLY the last leaf matters
if((this.getAbsoluteStart() + this.textLength) == (tcyParent.getAbsoluteStart() + tcyParent.textLength))
{
var tcyAdornBounds:Rectangle = new Rectangle();
tcyParent.calculateAdornmentBounds(tcyParent, tLine, blockProgression, tcyAdornBounds);
if (_computedFormat.textDecoration == TextDecoration.UNDERLINE)
{
g.lineStyle(metrics.underlineThickness, _computedFormat.color as uint);
var baseULAdjustment:Number = metrics.underlineOffset + (metrics.underlineThickness/2);
var xCoor:Number = adornRight ? spanBounds.right : spanBounds.left;
if(!adornRight)
baseULAdjustment = -baseULAdjustment;
g.moveTo(xCoor + baseULAdjustment, tcyAdornBounds.top);
g.lineTo(xCoor + baseULAdjustment, tcyAdornBounds.bottom);
}
if (_computedFormat.lineThrough == true)
{
var tcyMid:Number = spanBounds.bottomRight.x - tcyAdornBounds.x;
tcyMid /= 2;
tcyMid += tcyAdornBounds.x;
g.lineStyle(metrics.strikethroughThickness, _computedFormat.color as uint);
g.moveTo(tcyMid, tcyAdornBounds.top);
g.lineTo(tcyMid, tcyAdornBounds.bottom);
}
}
}
}
}
// Add the shape as a child of the TextLine. In some cases, the textLine we passed through may not be the same one that's
// in the TextFlowLine textLineCache. This can happen if the TextFlowLine's textLine is invalid and a child of the
// container. In that case, we generated a valid TextLine and passed it in as tLine so that we have correct metrics
// for generating the adornment shapes.
if (shape)
{
var peekLine:TextLine = (tLine.userData as TextFlowLine).peekTextLine();
CONFIG::debug{ assert(peekLine == null || peekLine == tLine || peekLine.validity == TextLineValidity.INVALID, "Valid/invalid mismatch"); }
// CONFIG::debug{ assert(peekLine == null || peekLine == TextFlowLine(tLine.userData).getTextLine(false), "Valid/invalid mismatch"); }
if (peekLine && tLine != peekLine)
{
// belief is that this line of code is never hit
CONFIG::debug { assert(false,"updateAdornmentsOnBounds: peekLine doesn't match textLine"); }
tLine = peekLine;
}
tLine.addChild(shape);
}
}
/** @private
* Adds the background rectangle (if needed), making adjustments for glyph shifting as appropriate
*/
private function addBackgroundRect(tLine:TextLine, metrics:FontMetrics, spanBounds:Rectangle, horizontalText:Boolean, isTCY:Boolean=false):void
{
if(_computedFormat.backgroundAlpha == 0 || _computedFormat.backgroundColor == BackgroundColor.TRANSPARENT)
return;
var tf:TextFlow = this.getTextFlow();
// ensure the TextFlow has a background manager - but its only supported with the StandardFlowComposer at this time
if (!tf.getBackgroundManager())
return;
// The background rectangle usually needs to coincide with the passsed-in span bounds.
var r:Rectangle = spanBounds.clone();
// With constrained glyph shifting (such as when superscript/subscript are in use), we'd like the
// background rectangle to follow the glyphs. Not so for arbitrary glyph shifting (such as when
// baseline shift or baseline alignment are in use)
// TODO-06/12/2009: Need to figure out adjustment for TCY background rect. No adjustment for now.
if (!isTCY && (_computedFormat.baselineShift == BaselineShift.SUPERSCRIPT || _computedFormat.baselineShift == BaselineShift.SUBSCRIPT))
{
// The atom bounds returned by FTE do not reflect the effect of glyph shifting.
// We approximate this effect by making the following assumptions (strikethrough/underline code does the same)
// - The strike-through adornment runs through the center of the glyph
// - The Roman baseline is halfway between the center and bottom (descent)
// Effectively, the glyph's descent equals the strike-through offset, and its ascent is three times that
var desiredExtent:Number; // The desired extent of the rectangle in the block progression direction
var baselineShift:Number;
var fontSize:Number = getEffectiveFontSize();
var baseStrikethroughOffset:Number = metrics.strikethroughOffset + metrics.strikethroughThickness/2;
if (_computedFormat.baselineShift == BaselineShift.SUPERSCRIPT)
{
// The rectangle needs to sit on the line's descent and must extend far enough to accommodate the
// ascender of the glyph (that has moved up because of superscript)
var glyphAscent:Number = -3 * baseStrikethroughOffset; // see assumptions above
baselineShift = -metrics.superscriptOffset * fontSize;
var lineDescent:Number = tLine.getBaselinePosition(TextBaseline.DESCENT) - tLine.getBaselinePosition(TextBaseline.ROMAN);
desiredExtent = glyphAscent + baselineShift + lineDescent;
if (horizontalText)
{
if (desiredExtent > r.height)
{
r.y -= desiredExtent - r.height;
r.height = desiredExtent;
}
}
else
{
if (desiredExtent > r.width)
r.width = desiredExtent;
}
}
else
{
// The rectangle needs to hang from the line's ascent and must extend far enough to accommodate the
// descender of the glyph (that has moved down because of superscript)
var glyphDescent:Number = -baseStrikethroughOffset; // see assumptions above
baselineShift = metrics.subscriptOffset * fontSize;
var lineAscent:Number = tLine.getBaselinePosition(TextBaseline.ROMAN) - tLine.getBaselinePosition(TextBaseline.ASCENT);
desiredExtent = lineAscent + baselineShift + glyphDescent;
if (horizontalText)
{
if (desiredExtent > r.height)
r.height = desiredExtent;
}
else
{
if (desiredExtent > r.width)
{
r.x -= desiredExtent - r.width
r.width = desiredExtent;
}
}
}
}
tf.backgroundManager.addRect(tLine, this, r, _computedFormat.backgroundColor, _computedFormat.backgroundAlpha);
}
/** @private */
tlf_internal function calculateStrikeThrough(textLine:TextLine, blockProgression:String, metrics:FontMetrics):Number
{
var underlineAndStrikeThroughShift:int = 0;
var effectiveFontSize:Number = this.getEffectiveFontSize()
if (_computedFormat.baselineShift == BaselineShift.SUPERSCRIPT)
{
underlineAndStrikeThroughShift = -(metrics.superscriptOffset * effectiveFontSize);
} else if (_computedFormat.baselineShift == BaselineShift.SUBSCRIPT)
{
underlineAndStrikeThroughShift = -(metrics.subscriptOffset * (effectiveFontSize / metrics.subscriptScale));
} else {
underlineAndStrikeThroughShift = TextLayoutFormat.baselineShiftProperty.computeActualPropertyValue(_computedFormat.baselineShift, effectiveFontSize);
}
//grab the dominantBaseline and alignmentBaseline strings
var domBaselineString:String = resolveDomBaseline(computedFormat, getParagraph());
var alignmentBaselineString:String = this.computedFormat.alignmentBaseline;
//this value represents the position of the baseline used to align this text
var alignDomBaselineAdjustment:Number = textLine.getBaselinePosition(domBaselineString);
//if the alignment baseline differs from the dominant, then we need to apply the delta between the
//dominant and the alignment to determine the line along which the glyphs are lining up...
if(alignmentBaselineString != flash.text.engine.TextBaseline.USE_DOMINANT_BASELINE &&
alignmentBaselineString != domBaselineString)
{
alignDomBaselineAdjustment = textLine.getBaselinePosition(alignmentBaselineString);
//take the alignmentBaseline offset and make it relative to the dominantBaseline
}
//first, establish the offset relative to the glyph based in fontMetrics data
var stOffset:Number = metrics.strikethroughOffset;
//why are we using the stOffset? Well, the stOffset effectively tells us where the mid-point
//of the glyph is. By using this value, we can determine how we need to offset the underline.
//now adjust the value. If it is center, then the glyphs are aligned along the ST position already
if(domBaselineString == flash.text.engine.TextBaseline.IDEOGRAPHIC_CENTER)
{
stOffset = 0;
}
else if(domBaselineString == flash.text.engine.TextBaseline.IDEOGRAPHIC_TOP ||
domBaselineString == flash.text.engine.TextBaseline.ASCENT)
{
stOffset *= -2; //if the glyphs are top or ascent, then we need to invert and double the offset
stOffset -= (2 * metrics.strikethroughThickness);
}
else if(domBaselineString == flash.text.engine.TextBaseline.IDEOGRAPHIC_BOTTOM ||
domBaselineString == flash.text.engine.TextBaseline.DESCENT)
{
stOffset *= 2; //if they're bottom, then we need to simply double it
stOffset += (2 * metrics.strikethroughThickness);
}
else //Roman
{
stOffset -= metrics.strikethroughThickness;
}
//Now apply the actual dominant baseline position to the offset
stOffset += (alignDomBaselineAdjustment - underlineAndStrikeThroughShift);
return stOffset;
}
/** @private */
tlf_internal function calculateUnderlineOffset(stOffset:Number, blockProgression:String, metrics:FontMetrics, textLine:TextLine):Number
{
var ulOffset:Number = metrics.underlineOffset + metrics.underlineThickness;
var baseSTAdjustment:Number = metrics.strikethroughOffset;
//based on the stOffset - which really represents the middle of the glyph, set the ulOffset
//which will always be relative. Note that simply using the alignDomBaselineAdjustment is not enough
if(blockProgression != BlockProgression.RL)
ulOffset += (stOffset - baseSTAdjustment) + metrics.underlineThickness/2;
else
{
var para:ParagraphElement = this.getParagraph();
if (para.computedFormat.locale.toLowerCase().indexOf("zh") == 0)
{
ulOffset = -ulOffset;
ulOffset -= (stOffset - baseSTAdjustment + (metrics.underlineThickness*2));
}
else
ulOffset -= (-ulOffset + stOffset + baseSTAdjustment + (metrics.underlineThickness/2));
}
return ulOffset;
}
/** @private */
CONFIG::debug public override function debugCheckFlowElement(depth:int = 0, extraData:String = ""):int
{
// debugging function that asserts if the flow element tree is in an invalid state
var rslt:int = super.debugCheckFlowElement(depth," fte:"+getDebugIdentity(_blockElement)+" "+extraData);
// TODO: eventually these tests will be valid for InlineGraphicElement elements as well
if (!(this is InlineGraphicElement))
{
rslt += assert(textLength != 0 || bindableElement || (parent is SubParagraphGroupElementBase && parent.numChildren == 1), "FlowLeafElement with zero textLength must be deleted");
rslt += assert(parent is ParagraphElement || parent is SubParagraphGroupElementBase, "FlowLeafElement must have a ParagraphElement or SubParagraphGroupElementBase parent");
}
return rslt;
}
}
}