blob: dbb01b8c349ae3c38cf44778d3364360c8a88d78 [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.Shape;
import flash.text.engine.ContentElement;
import flash.text.engine.EastAsianJustifier;
import flash.text.engine.GroupElement;
import flash.text.engine.LineJustification;
import flash.text.engine.SpaceJustifier;
import flash.text.engine.TabAlignment;
import flash.text.engine.TabStop;
import flash.text.engine.TextBaseline;
import flash.text.engine.TextBlock;
import flash.text.engine.TextLine;
import flash.text.engine.TextLineValidity;
import flash.text.engine.TextRotation;
import flash.utils.Dictionary;
import flash.utils.getQualifiedClassName;
import flashx.textLayout.compose.TextFlowLine;
import flashx.textLayout.container.ContainerController;
import flashx.textLayout.debug.Debugging;
import flashx.textLayout.debug.assert;
import flashx.textLayout.formats.BlockProgression;
import flashx.textLayout.formats.Direction;
import flashx.textLayout.formats.FormatValue;
import flashx.textLayout.formats.ITextLayoutFormat;
import flashx.textLayout.formats.JustificationRule;
import flashx.textLayout.formats.LeadingModel;
import flashx.textLayout.formats.LineBreak;
import flashx.textLayout.formats.TabStopFormat;
import flashx.textLayout.formats.TextAlign;
import flashx.textLayout.formats.TextJustify;
import flashx.textLayout.formats.TextLayoutFormat;
import flashx.textLayout.property.Property;
import flashx.textLayout.tlf_internal;
import flashx.textLayout.utils.CharacterUtil;
import flashx.textLayout.utils.LocaleUtil;
use namespace tlf_internal;
/**
* The ParagraphElement class represents a paragraph in the text flow hierarchy. Its parent
* is a ParagraphFormattedElement, and its children can include spans (SpanElement), images
* (inLineGraphicElement), links (LinkElement) and TCY (Tatechuuyoko - ta-tae-chu-yo-ko) elements (TCYElement). The
* paragraph text is stored in one or more SpanElement objects, which define ranges of text that share the same attributes.
* A TCYElement object defines a small run of Japanese text that runs perpendicular to the line, as in a horizontal run of text in a
* vertical line. A TCYElement can also contain multiple spans.
*
* @includeExample examples\ParagraphElementExample.as -noswf
* @includeExample examples\ParagraphElementExample2.as -noswf
*
* @playerversion Flash 10
* @playerversion AIR 1.5
* @langversion 3.0
*
* @see InlineGraphicElement
* @see LinkElement
* @see SpanElement
* @see TCYElement
* @see TextFlow
*/
public final class ParagraphElement extends ParagraphFormattedElement
{
//private var _textBlock:TextBlock;
private var _textBlockChildren:Dictionary;
private var _terminatorSpan:SpanElement;
private var _interactiveChildrenCount:int;
/** Constructor - represents a paragraph in a text flow.
*
* @playerversion Flash 10
* @playerversion AIR 1.5
* @langversion 3.0
*/
public function ParagraphElement()
{
super();
_terminatorSpan = null;
_interactiveChildrenCount = 0 ;
_textBlockChildren = new Dictionary();
}
tlf_internal function get interactiveChildrenCount():int
{
return _interactiveChildrenCount;
}
/** @private */
tlf_internal function createTextBlock():void
{
CONFIG::debug { assert(_textBlock == null,"createTextBlock called when there is already a textblock"); }
computedFormat; // recreate the format BEFORE the _textBlock is created
var tbs:Vector.<TextBlock> = getTextBlocks();
//tbs.length = 0;
var tableCount:int = 0;
if(tbs.length == 0 && !(getChildAt(0) is TableElement) )
tbs.push(new TextBlock());
//getTextBlocks()[0] = new TextBlock();
CONFIG::debug { Debugging.traceFTECall(_textBlock,null,"new TextBlock()"); }
for (var i:int = 0; i < numChildren; i++)
{
var child:FlowElement = getChildAt(i);
if(child is TableElement)
tableCount++;
// tbs.push(new TextBlock());
else
{
//child.releaseContentElement();
//child.createContentElement();
}
}
while(tableCount >= tbs.length)
tbs.push(new TextBlock());
for (i = 0; i < numChildren; i++)
{
child = getChildAt(i);
child.createContentElement();
}
tbs.length = tableCount + 1;
var tb:TextBlock;
for each(tb in tbs){
updateTextBlock(tb);
}
}
private function updateTextBlockDict():void
{
var tbs:Vector.<TextBlock> = getTextBlocks();
if(tbs.length == 0)
return;//nothing to do
var tbIdx:int = 0;
var tb:TextBlock = tbs[tbIdx];
var items:Array = [];
var child:FlowElement;
for (var i:int = 0; i < numChildren; i++)
{
child = getChildAt(i);
if(child is TableElement)
{
_textBlockChildren[tb] = items;
tb = tbs[++tbIdx];
items = [];
continue;
}
items.push(child);
}
_textBlockChildren[tb] = items;
}
private function removeTextBlock(tb:TextBlock):void
{
var tbs:Vector.<TextBlock> = getTextBlocks();
if(tbs)
{
var idx:int = getTextBlocks().indexOf(tb);
if(idx > -1)
{
tbs.splice(idx,1);
delete _textBlockChildren[tb];
}
}
}
private function releaseTextBlockInternal(tb:TextBlock):void
{
if (!tb)
return;
if (tb.firstLine) // A TextBlock may have no firstLine if it has previously been released.
{
for (var textLineTest:TextLine = tb.firstLine; textLineTest != null; textLineTest = textLineTest.nextLine)
{
if(textLineTest.numChildren != 0)
{
//if the number of adornments added does not match the number of children on the textLine
//then a third party has added adornments. Don't recycle the line or the adornment will be
//lost.
var tfl:TextFlowLine = textLineTest.userData as TextFlowLine;
if(tfl.adornCount != textLineTest.numChildren)
return;
}
}
CONFIG::debug { Debugging.traceFTECall(null,tb,"releaseLines",tb.firstLine, tb.lastLine); }
tb.releaseLines(tb.firstLine, tb.lastLine);
}
var items:Array = _textBlockChildren[tb];
var len:int = items.length;
for (var i:int = 0; i < len; i++)
{
var child:FlowElement = items[i];
child.releaseContentElement();
}
items.length = 0;
tb.content = null;
removeTextBlock(tb);
}
/** @private */
tlf_internal function releaseTextBlock(tb:TextBlock=null):void
{
updateTextBlockDict();
if(tb)
{
releaseTextBlockInternal(tb);
return;
}
var tbs:Vector.<TextBlock> = getTextBlocks();
for each(var textBlock:TextBlock in tbs)
{
releaseTextBlockInternal(textBlock);
}
//_textBlock = null;
if (_computedFormat)
_computedFormat = null;
}
private var _textBlocks:Vector.<TextBlock>;
tlf_internal function getTextBlocks():Vector.<TextBlock>
{
if(_textBlocks == null)
_textBlocks = new Vector.<TextBlock>();
return _textBlocks;
}
/** TextBlock where the text of the paragraph is kept. @private */
tlf_internal function getTextBlock():TextBlock
{
if (!getTextBlocks().length)
createTextBlock();
return getTextBlocks()[0];
}
/** Last TextBlock where the text of the paragraph is kept. @private */
tlf_internal function getLastTextBlock():TextBlock
{
var tbs:Vector.<TextBlock> = getTextBlocks();
if(!tbs.length)
createTextBlock();
return tbs[tbs.length-1];
}
/** Get TextBlock at specified position. @private */
tlf_internal function getTextBlockAtPosition(pos:int):TextBlock
{
var curPos:int = 0;
var posShift:int = 0;
var tables:Vector.<TableElement> = getTables();
if(!tables.length)
return getTextBlock();
for each(var table:TableElement in tables)
{
if(table.getElementRelativeStart(this) < pos)
posShift++;
}
var tbs:Vector.<TextBlock> = getTextBlocks();
for each(var tb:TextBlock in tbs)
{
if(tb.content == null)
return tb;
curPos += tb.content.rawText.length;
if(curPos + posShift > pos)
{
if(getTextBlockStart(tb) > pos)
return null;
return tb;
}
}
return null;
}
tlf_internal function getTextBlockAbsoluteStart(tb:TextBlock):int
{
var start:int = getTextBlockStart(tb);
if(start < 0)
start = 0;
return getAbsoluteStart() + start;
}
tlf_internal function getTextBlockStart(tb:TextBlock):int
{
var i:int;
var curPos:int = 0;
var tbs:Vector.<TextBlock> = getTextBlocks();
if(tbs.length == 0)
return -1;
var tables:Vector.<TableElement> = getTables();
for each(var curTB:TextBlock in tbs)
{
for each(var table:TableElement in tables)
{
if(table.getElementRelativeStart(this) <= curPos)
{
curPos++;
tables.splice(tables.indexOf(table),1);
}
}
if(tb == curTB)
return curPos;
if(tb.content)
curPos += curTB.content.rawText.length;
}
return -1;
}
private function getTables():Vector.<TableElement>
{
var tables:Vector.<TableElement> = new Vector.<TableElement>();
for (var i:int = 0; i < numChildren; i++)
{
var child:FlowElement = getChildAt(i);
if(child is TableElement)
tables.push(child as TableElement);
}
return tables;
}
/** TextBlock where the text of the paragraph is kept, or null if we currently don't have one. @private */
tlf_internal function peekTextBlock():TextBlock
{
return getTextBlocks().length == 0 ? null : getTextBlocks()[0];
}
/** @private */
tlf_internal function releaseLineCreationData():void
{
CONFIG::debug { assert(Configuration.playerEnablesArgoFeatures,"bad call to releaseLineCreationData"); }
var tbs:Vector.<TextBlock> = getTextBlocks();
for each(var tb:TextBlock in tbs)
{
tb["releaseLineCreationData"]();
}
}
/** @private */
tlf_internal override function createContentAsGroup(pos:int=0):GroupElement
{
var tb:TextBlock = getTextBlockAtPosition(pos);
if(!tb)
tb = getTextBlockAtPosition(pos-1);
var group:GroupElement = tb.content as GroupElement;
if (!group)
{
var originalContent:ContentElement = tb.content;
group = new GroupElement();
CONFIG::debug { Debugging.traceFTECall(group,null,"new GroupElement()"); }
tb.content = group;
CONFIG::debug { Debugging.traceFTEAssign(tb,"content",group); }
if (originalContent)
{
var gc:Vector.<ContentElement> = new Vector.<ContentElement>();
CONFIG::debug { Debugging.traceFTECall(gc,null,"new Vector.<ContentElement>()") }
gc.push(originalContent);
CONFIG::debug { Debugging.traceFTECall(null,gc,"push",originalContent); }
group.replaceElements(0,0,gc);
CONFIG::debug { Debugging.traceFTECall(null,group,"replaceElements",0,0,gc); }
}
// Now we've got to force damage the entire paragraph, because we restructured it in FTE.
if (tb.firstLine && textLength)
{
var textFlow:TextFlow = getTextFlow();
if (textFlow)
textFlow.damage(getAbsoluteStart(), textLength, TextLineValidity.INVALID, false);
}
}
return group;
}
/** @private */
tlf_internal override function removeBlockElement(child:FlowElement, block:ContentElement):void
{
var tb:TextBlock = getTextBlockAtPosition(child.getElementRelativeStart(this));
if(!tb)
tb = getTextBlock();
if(tb.content == null)
return;
var relativeStart:int = child.getElementRelativeStart(this);
if (getChildrenInTextBlock(relativeStart).length < 2)
{
if (block is GroupElement)
{
// see insertBlockElement
CONFIG::debug { assert(_textBlock.content != block,"removeBlockElement: bad call to removeBlockElement"); }
CONFIG::debug { assert(_textBlock.content is GroupElement,"removeBlockElement: bad content"); }
CONFIG::debug { assert(GroupElement(_textBlock.content).elementCount == 1,"removeBlockElement: bad element count"); }
CONFIG::debug { assert(GroupElement(_textBlock.content).getElementAt(0) == block,"removeBlockElement: bad group content"); }
GroupElement(tb.content).replaceElements(0,1,null);
CONFIG::debug { Debugging.traceFTECall(null,_textBlock.content,"replaceElements",0,1,null); }
}
tb.content = null;
CONFIG::debug { Debugging.traceFTEAssign(_textBlock,"content",null); }
}
else if(block.groupElement)
{
var idx:int = getChildIndexInBlock(child);
var group:GroupElement = GroupElement(tb.content);
CONFIG::debug { assert(group.elementCount == numChildren,"Mismatched group and elementCount"); }
group.replaceElements(idx,idx+1,null);
if(group.elementCount == 0)
return;
CONFIG::debug { Debugging.traceFTECall(null,group,"replaceElements",idx,idx+1,null); }
if (numChildren == 2) // its going to be one so ungroup
{
// ungroup - need to take it out first as inlinelements can only have one parent
var elem:ContentElement = group.getElementAt(0);
CONFIG::debug { Debugging.traceFTECall(elem,group,"getElementAt",0); }
if (!(elem is GroupElement))
{
group.replaceElements(0,1,null);
CONFIG::debug { Debugging.traceFTECall(null,group,"replaceElements",0,1,null); }
tb.content = elem;
CONFIG::debug { Debugging.traceFTEAssign(tb,"content",elem); }
}
}
}
else {
//trace("1");
//tb.content = null;
}
}
/** @private */
tlf_internal override function hasBlockElement():Boolean
{
return getTextBlocks().length > 0;
}
/** @private */
override tlf_internal function createContentElement():void
{
createTextBlock();
}
/** @private */
private function getChildrenInTextBlock(pos:int):Array
{
var retVal:Array = [];
if(numChildren == 0)
return retVal;
if(numChildren == 1)
{
retVal.push(getChildAt(0));
return retVal
}
var chldrn:Array = mxmlChildren.slice();
for(var i:int = 0; i<chldrn.length;i++)
{
if(chldrn[i] is TableElement)
{
if(chldrn[i].parentRelativeStart < pos)
{
retVal.length = 0;
continue;
}
if(chldrn[i].parentRelativeStart >= pos)
break;
}
retVal.push(chldrn[i]);
}
return retVal;
}
/** @private */
tlf_internal override function insertBlockElement(child:FlowElement, block:ContentElement):void
{
var relativeStart:int = child.getElementRelativeStart(this);
var tb:TextBlock = getTextBlockAtPosition(relativeStart);
if(!tb)
tb = getTextBlockAtPosition(relativeStart-1);
if(!tb)
{
child.releaseContentElement();
return;
}
if (getTextBlocks().length == 0)
{
child.releaseContentElement();
createTextBlock(); // does the whole tree
return;
}
var gc:Vector.<ContentElement>; // scratch var
var group:GroupElement; // scratch
if (getChildrenInTextBlock(relativeStart).length < 2)
{
if (block is GroupElement)
{
// this case forces the Group to be in a Group so that following FlowLeafElements aren't in a SubParagraphGroupElementBase's group
gc = new Vector.<ContentElement>();
CONFIG::debug { Debugging.traceFTECall(gc,null,"new Vector.<ContentElement>()") }
gc.push(block);
CONFIG::debug { Debugging.traceFTECall(null,gc,"push",block); }
group = new GroupElement(gc);
CONFIG::debug { Debugging.traceFTECall(group,null,"new GroupElement",gc); }
tb.content = group;
CONFIG::debug { Debugging.traceFTEAssign(_textBlock,"content",group); }
}
else
{
if(block.groupElement)
{
block.groupElement.elementCount;
}
tb.content = block;
CONFIG::debug { Debugging.traceFTEAssign(_textBlock,"content",block); }
}
}
else
{
group = createContentAsGroup(relativeStart);
var idx:int = getChildIndexInBlock(child);
gc = new Vector.<ContentElement>();
CONFIG::debug { Debugging.traceFTECall(gc,null,"new Vector.<ContentElement>") }
gc.push(block);
CONFIG::debug { Debugging.traceFTECall(null,gc,"push",block); }
// If elements in the middle (i.e. ones in the process of being added) were missed, the idx can be too high.
// The missed ones will be inserted later.
if(idx > group.elementCount)
idx = group.elementCount;
group.replaceElements(idx,idx,gc);
CONFIG::debug { Debugging.traceFTECall(null,group,"replaceElements",idx,idx,gc); }
}
}
private function getChildIndexInBlock(elem:FlowElement):int
{
var relIdx:int = 0;
for (var i:int = 0; i < numChildren; i++)
{
var child:FlowElement = getChildAt(i);
if(child == elem)
return relIdx;
relIdx++;
if(child is TableElement)
relIdx = 0;
}
return -1;
}
/** @private */
override protected function get abstract():Boolean
{ return false; }
/** @private */
tlf_internal override function get defaultTypeName():String
{ return "p"; }
tlf_internal function removeEmptyTerminator():void
{
if(numChildren == 1 && _terminatorSpan && _terminatorSpan.textLength == 1)
{
_terminatorSpan.removeParaTerminator();
super.replaceChildren(0, 1);
this._terminatorSpan = null;
}
}
/** @private */
public override function replaceChildren(beginChildIndex:int,endChildIndex:int,...rest):void
{
var applyParams:Array;
do{
if(_terminatorSpan)
{
var termIdx:int = getChildIndex(_terminatorSpan);
if(termIdx > 0 && termIdx < beginChildIndex && _terminatorSpan.textLength == 1)
{
super.replaceChildren(termIdx, termIdx+1);
_terminatorSpan = null;
if(beginChildIndex >= termIdx)
{
beginChildIndex--;
if(rest.length == 0) // delete of terminator was already done.
break;
}
if(endChildIndex >= termIdx && beginChildIndex != endChildIndex)
endChildIndex--;
}
}
// makes a measurable difference - rest.length zero and one are the common cases
if (rest.length == 1)
applyParams = [beginChildIndex, endChildIndex, rest[0]];
else
{
applyParams = [beginChildIndex, endChildIndex];
if (rest.length != 0)
applyParams = applyParams.concat.apply(applyParams, rest);
}
super.replaceChildren.apply(this, applyParams);
}while(false);
ensureTerminatorAfterReplace();
// ensure correct text blocks
createTextBlock();
}
public override function splitAtPosition(relativePosition:int):FlowElement
{
// need to handle multiple TextBlocks
// maybe not. It might be handled in replaceChildren().
return super.splitAtPosition(relativePosition);
}
/** @private */
tlf_internal function ensureTerminatorAfterReplace():void
{
//lose reference to terminator if it was removed or not a direct child.
if(_terminatorSpan && _terminatorSpan.parent != this)
{
_terminatorSpan.removeParaTerminator();
_terminatorSpan = null;
}
var newLastLeaf:FlowLeafElement = getLastLeaf();
if (_terminatorSpan != newLastLeaf)
{
if(_terminatorSpan)
_terminatorSpan.removeParaTerminator();
if (newLastLeaf && _terminatorSpan)
{
if(_terminatorSpan.textLength == 0 && !_terminatorSpan.id)
{
var termIdx:int = getChildIndex(_terminatorSpan);
super.replaceChildren(termIdx, termIdx+1);
}
this._terminatorSpan = null;
}
if(newLastLeaf)
{
if (newLastLeaf is SpanElement)
{
(newLastLeaf as SpanElement).addParaTerminator();
this._terminatorSpan = newLastLeaf as SpanElement;
}
else
{
var s:SpanElement = new SpanElement();
super.replaceChildren(numChildren,numChildren,s);
s.format = newLastLeaf ? newLastLeaf.format : _terminatorSpan.format;
s.addParaTerminator();
this._terminatorSpan = s;
}
}
else
_terminatorSpan = null;
}
//merge terminator span to previous if possible
if(_terminatorSpan && _terminatorSpan.textLength == 1)
{
var prev:FlowLeafElement = _terminatorSpan.getPreviousLeaf(this);
if(prev && prev.parent == this && prev is SpanElement)
{
_terminatorSpan.mergeToPreviousIfPossible();
}
}
}
/** @private */
tlf_internal function updateTerminatorSpan(splitSpan:SpanElement,followingSpan:SpanElement):void
{
if (_terminatorSpan == splitSpan)
_terminatorSpan = followingSpan;
}
[RichTextContent]
/** @private NOTE: all FlowElement implementers and overrides of mxmlChildren must specify [RichTextContent] metadata */
public override function set mxmlChildren(array:Array):void
{
// remove all existing children
replaceChildren(0,numChildren);
for each (var child:Object in array)
{
if (child is FlowElement)
{
if ((child is SpanElement) || (child is SubParagraphGroupElementBase))
child.bindableElement = true;
// Note: calling super.replaceChildren because we don't want to transfer para terminator each time
super.replaceChildren(numChildren, numChildren, child as FlowElement);
}
else if (child is String)
{
var s:SpanElement = new SpanElement();
s.text = String(child);
s.bindableElement = true;
// Note: calling super.replaceChildren because we don't want to transfer para terminator each time
super.replaceChildren(numChildren, numChildren, s);
}
else if (child != null)
throw new TypeError(GlobalSettings.resourceStringFunction("badMXMLChildrenArgument",[ getQualifiedClassName(child) ]));
}
// Now ensure para terminator
ensureTerminatorAfterReplace();
// recreate text blocks to handle possible TableElement changes
createTextBlock();
}
/** @private
*/
public override function getText(relativeStart:int=0, relativeEnd:int=-1, paragraphSeparator:String="\n"):String
{
// Optimization for getting text of the entire paragraph
if (relativeStart == 0 && (relativeEnd == -1 || relativeEnd >= textLength-1) && getTextBlocks().length)
{
var tb:TextBlock;
var tbs:Vector.<TextBlock> = getTextBlocks();
var text:String = "";
for each(tb in tbs)
{
text = text + getTextInBlock(tb);
}
if(tb.content && tb.content.rawText)
return text.substring(0, text.length - 1);
return text;
}
return super.getText(relativeStart, relativeEnd, paragraphSeparator);
}
private function getTextInBlock(tb:TextBlock):String{
if(!tb.content || !tb.content.rawText)
return "";
return tb.content.rawText;
}
/** Returns the paragraph that follows this one, or null if there are no more paragraphs.
*
* @return the next paragraph or null if there are no more paragraphs.
*
* @includeExample examples\ParagraphElement_getNextParagraph.as -noswf
*
* @playerversion Flash 10
* @playerversion AIR 1.5
* @langversion 3.0
*
* @see #getPreviousParagraph()
*/
public function getNextParagraph():ParagraphElement
{
var nextLeaf:FlowLeafElement = getLastLeaf().getNextLeaf();
return nextLeaf ? nextLeaf.getParagraph() : null;
}
/** Returns the paragraph that precedes this one, or null, if this paragraph is the first one
* in the TextFlow.
*
* @includeExample examples\ParagraphElement_getPreviousParagraph.as -noswf
*
* @playerversion Flash 10
* @playerversion AIR 1.5
* @langversion 3.0
*
* @see #getNextParagraph()
*/
public function getPreviousParagraph():ParagraphElement
{
var previousLeaf:FlowLeafElement = getFirstLeaf().getPreviousLeaf();
return previousLeaf ? previousLeaf.getParagraph() : null;
}
/**
* Scans backward from the supplied position to find the location
* in the text of the previous atom and returns the index. The term atom refers to
* both graphic elements and characters (including groups of combining characters), the
* indivisible entities that make up a text line.
*
* @param relativePosition position in the text to start from, counting from 0
* @return index in the text of the previous cluster
*
* @includeExample examples\ParagraphElement_findPreviousAtomBoundary.as -noswf
*
* @playerversion Flash 10
* @playerversion AIR 1.5
* @langversion 3.0
*
* @see flash.text.engine.TextLine
*/
public function findPreviousAtomBoundary(relativePosition:int):int
{
var tb:TextBlock = getTextBlockAtPosition(relativePosition);
if(!tb || !tb.content)
return relativePosition-1;
var tbStart:int = getTextBlockStart(tb);
var textBlockPos:int = relativePosition - tbStart;
var tl:TextLine = tb.getTextLineAtCharIndex(textBlockPos);
if (ContainerController.tlf_internal::usesDiscretionaryHyphens && tl != null)
{
var currentAtomIndex:int = tl.getAtomIndexAtCharIndex(textBlockPos);
//trace("relpos", relativePosition, "atomIndex", currentAtomIndex);
var isRTL:Boolean = tl.getAtomBidiLevel(currentAtomIndex) == 1;
if (isRTL)
{
var foo:int = tb.findPreviousAtomBoundary(textBlockPos);
if (currentAtomIndex == 0)
{
// when cursor is left of all characters (end of line)
// atomIndex is 0, so compensate
if (tl.atomCount > 0)
{
while (--textBlockPos)
{
--relativePosition;
if (tl.getAtomIndexAtCharIndex(textBlockPos) != currentAtomIndex)
break;
}
}
}
else
{
while (--relativePosition && --textBlockPos)
{
if (tl.getAtomIndexAtCharIndex(textBlockPos) != currentAtomIndex)
break;
}
}
if (CharacterUtil.isLowSurrogate(getText(relativePosition, relativePosition + 1).charCodeAt(0)))
{
relativePosition--;
textBlockPos--;
}
//trace("previous", relativePosition, foo);
}
else
{
if (currentAtomIndex == 0)
{
tl = tl.previousLine;
if (!tl)
{
if(tb != _textBlocks[0])
return relativePosition-1;
return -1;
}
// need this when 0x2028 line separator in use
if (tl.textBlockBeginIndex + tl.rawTextLength == textBlockPos)
return tl.textBlockBeginIndex + tl.rawTextLength - 1 + tbStart;
return tl.textBlockBeginIndex + tl.rawTextLength + tbStart;
}
while (--relativePosition && --textBlockPos)
{
if (tl.getAtomIndexAtCharIndex(textBlockPos) < currentAtomIndex)
break;
}
if (CharacterUtil.isLowSurrogate(getText(relativePosition, relativePosition + 1).charCodeAt(0)))
{
relativePosition--;
textBlockPos--;
}
}
return relativePosition;
}
var pos:int = tb.findPreviousAtomBoundary(textBlockPos);
if(pos >= 0)
pos += tbStart;
//trace("previous", relativePosition, pos);
return pos;
}
/**
* Scans ahead from the supplied position to find the location
* in the text of the next atom and returns the index. The term atom refers to
* both graphic elements and characters (including groups of combining characters), the
* indivisible entities that make up a text line.
*
* @param relativePosition position in the text to start from, counting from 0
* @return index in the text of the following atom
*
* @includeExample examples\ParagraphElement_findNextAtomBoundary.as -noswf
*
* @playerversion Flash 10
* @playerversion AIR 1.5
* @langversion 3.0
*
* @see flash.text.engine.TextLine
*/
public function findNextAtomBoundary(relativePosition:int):int
{
var tb:TextBlock = getTextBlockAtPosition(relativePosition);
if(!tb || !tb.content)
return relativePosition+1;
var tbStart:int = getTextBlockStart(tb);
var textBlockPos:int = relativePosition - tbStart;
var tl:TextLine = tb.getTextLineAtCharIndex(textBlockPos);
if (ContainerController.tlf_internal::usesDiscretionaryHyphens && tl != null)
{
var currentAtomIndex:int = tl.getAtomIndexAtCharIndex(textBlockPos);
//trace("relpos", relativePosition, "atomIndex", currentAtomIndex);
var isRTL:Boolean = tl.getAtomBidiLevel(currentAtomIndex) == 1;
if (isRTL)
{
var foo:int = tb.findNextAtomBoundary(textBlockPos);
if (currentAtomIndex == 0)
{
while (++textBlockPos)
{
++relativePosition;
if (tl.getAtomIndexAtCharIndex(textBlockPos) != currentAtomIndex)
break;
}
}
else
{
while (++textBlockPos)
{
++relativePosition;
if (tl.getAtomIndexAtCharIndex(textBlockPos) != currentAtomIndex)
break;
}
}
if (CharacterUtil.isHighSurrogate(getText(relativePosition, relativePosition + 1).charCodeAt(0)))
{
relativePosition++;
textBlockPos++;
}
//trace("next", relativePosition, foo);
}
else
{
if (currentAtomIndex == tl.atomCount - 1)
{
tl = tl.nextLine;
if (!tl)
{
if(tb != _textBlocks[_textBlocks.length-1])
return relativePosition+1;
return -1;
}
return tl.textBlockBeginIndex + tbStart;
}
while (++textBlockPos)
{
++relativePosition;
if (tl.getAtomIndexAtCharIndex(textBlockPos) > currentAtomIndex)
break;
}
if (CharacterUtil.isHighSurrogate(getText(relativePosition, relativePosition + 1).charCodeAt(0)))
{
relativePosition++;
textBlockPos++;
}
}
return relativePosition;
}
var pos:int = tb.findNextAtomBoundary(textBlockPos);
if(pos >= 0)
pos += tbStart;
//trace("next", relativePosition, pos);
return pos;
}
/** @private */
public override function getCharAtPosition(relativePosition:int):String
{
var foundTB:TextBlock = getTextBlockAtPosition(relativePosition);
if(!foundTB)
return "\u0016";
var tables:Vector.<TableElement> = getTables();
var pos:int = relativePosition;
for each(var table:TableElement in tables)
{
if(table.getElementRelativeStart(this) < pos)
relativePosition--;
}
var tbs:Vector.<TextBlock> = getTextBlocks();
for each(var tb:TextBlock in tbs)
{
if(foundTB == tb)
break;
if(tb)
relativePosition -= tb.content.rawText.length;
else
relativePosition -= 1;this.getText()
}
return foundTB.content.rawText.charAt(relativePosition);
}
/**
* Returns the index of the previous word boundary in the text.
*
* <p>Scans backward from the supplied position to find the previous position
* in the text that starts or ends a word. </p>
*
* @param relativePosition position in the text to start from, counting from 0
* @return index in the text of the previous word boundary
*
* @includeExample examples\ParagraphElement_findPreviousWordBoundary.as -noswf
*
* @playerversion Flash 10
* @playerversion AIR 1.5
* @langversion 3.0
*/
public function findPreviousWordBoundary(relativePosition:int):int
{
if (relativePosition == 0)
return 0;
var prevCharCode:int = getCharCodeAtPosition(relativePosition - 1);
if (CharacterUtil.isWhitespace(prevCharCode))
{
while (CharacterUtil.isWhitespace(prevCharCode) && ((relativePosition - 1) > 0))
{
relativePosition--;
prevCharCode = getCharCodeAtPosition(relativePosition - 1);
}
return relativePosition;
}
var block:TextBlock = getTextBlockAtPosition(relativePosition);
if(block == null)
block = getTextBlockAtPosition(--relativePosition);
var pos:int = getTextBlockStart(block);
if(pos < 0)
pos = 0;
return relativePosition == pos ? pos : pos + block.findPreviousWordBoundary(relativePosition - pos);
}
/**
* Returns the index of the next word boundary in the text.
*
* <p>Scans ahead from the supplied position to find the next position
* in the text that starts or ends a word. </p>
*
* @param relativePosition position in the text to start from, counting from 0
* @return index in the text of the next word boundary
*
* @includeExample examples\ParagraphElement_findNextWordBoundary.as -noswf
*
* @playerversion Flash 10
* @playerversion AIR 1.5
* @langversion 3.0
*/
public function findNextWordBoundary(relativePosition:int):int
{
if (relativePosition == textLength)
return textLength;
var curCharCode:int = getCharCodeAtPosition(relativePosition);
if (CharacterUtil.isWhitespace(curCharCode))
{
while (CharacterUtil.isWhitespace(curCharCode) && relativePosition < (textLength - 1))
{
relativePosition++;
curCharCode = getCharCodeAtPosition(relativePosition);
}
return relativePosition;
}
var block:TextBlock = getTextBlockAtPosition(relativePosition);
if(block == null)
block = getTextBlockAtPosition(--relativePosition);
var pos:int = getTextBlockStart(block);
if(pos < 0)
pos = 0;
return pos + block.findNextWordBoundary(relativePosition - pos);
}
static private var _defaultTabStops:Vector.<TabStop>;
static private const defaultTabWidth:int = 48; // matches default tabs setting in Argo
static private const defaultTabCount:int = 20;
static private function initializeDefaultTabStops():void
{
_defaultTabStops = new Vector.<TabStop>(defaultTabCount, true);
for (var i:int = 0; i < defaultTabCount; ++i)
_defaultTabStops[i] = new TabStop(TextAlign.START, defaultTabWidth * i);
}
private function updateTextBlock(textBlock:TextBlock=null):void
{
if(!textBlock)
textBlock = getTextBlock();
// find the ancestor with a container and use its format for various settings
var containerElement:ContainerFormattedElement = getAncestorWithContainer();
if (!containerElement)
return;
var containerElementFormat:ITextLayoutFormat = containerElement ? containerElement.computedFormat : TextLayoutFormat.defaultFormat;
var lineJust:String;
if (computedFormat.textAlign == TextAlign.JUSTIFY)
{
lineJust = (_computedFormat.textAlignLast == TextAlign.JUSTIFY) ?
LineJustification.ALL_INCLUDING_LAST :
LineJustification.ALL_BUT_LAST;
// We don't allow explicit line breaks and justification together because it results in very strange (invisible) lines
if (containerElementFormat.lineBreak == LineBreak.EXPLICIT)
lineJust = LineJustification.UNJUSTIFIED;
}
else
lineJust = LineJustification.UNJUSTIFIED;
var makeJustRuleStyle:String = this.getEffectiveJustificationStyle();
var justRule:String = this.getEffectiveJustificationRule();
// set the justifier in the TextBlock
if (justRule == JustificationRule.SPACE)
{
var spaceJustifier:SpaceJustifier = new SpaceJustifier(_computedFormat.locale,lineJust,false);
spaceJustifier.letterSpacing = _computedFormat.textJustify == TextJustify.DISTRIBUTE ? true : false;
if (Configuration.playerEnablesArgoFeatures)
{
// These three properties have to be set in the correct order so that consistency checks done
// in the Player on set are never violated
var newMinimumSpacing:Number = Property.toNumberIfPercent(_computedFormat.wordSpacing.minimumSpacing)/100;
var newMaximumSpacing:Number = Property.toNumberIfPercent(_computedFormat.wordSpacing.maximumSpacing)/100;
var newOptimumSpacing:Number = Property.toNumberIfPercent(_computedFormat.wordSpacing.optimumSpacing)/100;
spaceJustifier["minimumSpacing"] = Math.min(newMinimumSpacing, spaceJustifier["minimumSpacing"]);
spaceJustifier["maximumSpacing"] = Math.max(newMaximumSpacing, spaceJustifier["maximumSpacing"]);
spaceJustifier["optimumSpacing"] = newOptimumSpacing;
spaceJustifier["minimumSpacing"] = newMinimumSpacing;
spaceJustifier["maximumSpacing"] = newMaximumSpacing;
}
CONFIG::debug { Debugging.traceFTECall(spaceJustifier,null,"new SpaceJustifier",_computedFormat.locale,lineJust,spaceJustifier.letterSpacing); }
textBlock.textJustifier = spaceJustifier;
CONFIG::debug { Debugging.traceFTEAssign(textBlock,"textJustifier",spaceJustifier); }
textBlock.baselineZero = getLeadingBasis(this.getEffectiveLeadingModel());
CONFIG::debug { Debugging.traceFTEAssign(textBlock,"baselineZero",textBlock.baselineZero); }
}
else
{
var eastAsianJustifier:Object = new EastAsianJustifier(_computedFormat.locale,lineJust, makeJustRuleStyle);
if( Configuration.versionIsAtLeast(10,3) && eastAsianJustifier.hasOwnProperty("composeTrailingIdeographicSpaces")){
eastAsianJustifier.composeTrailingIdeographicSpaces = true;
}
CONFIG::debug { Debugging.traceFTECall(eastAsianJustifier,null,"new EastAsianJustifier",_computedFormat.locale,lineJust,makeJustRuleStyle); }
textBlock.textJustifier = eastAsianJustifier as EastAsianJustifier;
CONFIG::debug { Debugging.traceFTEAssign(textBlock,"textJustifier",eastAsianJustifier); }
textBlock.baselineZero = getLeadingBasis(this.getEffectiveLeadingModel());
CONFIG::debug { Debugging.traceFTEAssign(textBlock,"baselineZero",textBlock.baselineZero); }
}
textBlock.bidiLevel = _computedFormat.direction == Direction.LTR ? 0 : 1;
CONFIG::debug { Debugging.traceFTEAssign(textBlock,"bidiLevel",textBlock.bidiLevel); }
textBlock.lineRotation = containerElementFormat.blockProgression == BlockProgression.RL ? TextRotation.ROTATE_90 : TextRotation.ROTATE_0;
CONFIG::debug { Debugging.traceFTEAssign(textBlock,"lineRotation",textBlock.lineRotation); }
if (_computedFormat.tabStops && _computedFormat.tabStops.length != 0)
{
//create a vector of TabStops and assign it to tabStops in textBlock
var tabStops:Vector.<TabStop> = new Vector.<TabStop>();
CONFIG::debug { Debugging.traceFTECall(tabStops,null,"new Vector.<TabStop>()"); }
for each(var tsa:TabStopFormat in _computedFormat.tabStops)
{
var token:String = tsa.decimalAlignmentToken==null ? "" : tsa.decimalAlignmentToken;
var alignment:String = tsa.alignment==null ? TabAlignment.START : tsa.alignment;
var tabStop:TabStop = new TabStop(alignment,Number(tsa.position),token);
// this next line when uncommented works around bug 1912782
if (tsa.decimalAlignmentToken != null) var garbage:String = "x" + tabStop.decimalAlignmentToken;
CONFIG::debug { Debugging.traceFTECall(tabStop,null,"new TabStop",tabStop.alignment,tabStop.position,tabStop.decimalAlignmentToken); }
tabStops.push(tabStop);
CONFIG::debug { Debugging.traceFTECall(null,tabStops,"push",tabStop); }
}
textBlock.tabStops = tabStops;
CONFIG::debug { Debugging.traceFTEAssign(textBlock,"tabStops",tabStops); }
}
else if (GlobalSettings.enableDefaultTabStops && !Configuration.playerEnablesArgoFeatures)
{
// Player versions prior to 10.1 do not set up any default tabStops. As a workaround, if enableDefaultTabs
// is true, TLF will set up default tabStops in the case where there are no tabs defined.
if (_defaultTabStops == null)
initializeDefaultTabStops();
textBlock.tabStops = _defaultTabStops;
CONFIG::debug { Debugging.traceFTEAssign(textBlock,"tabStops",_defaultTabStops); }
}
else
{
textBlock.tabStops = null;
CONFIG::debug { Debugging.traceFTEAssign(textBlock,"tabStops",null); }
}
}
/** @private */
public override function get computedFormat():ITextLayoutFormat
{
if (!_computedFormat)
{
super.computedFormat;
var tbs:Vector.<TextBlock> = getTextBlocks();
for each(var tb:TextBlock in tbs)
updateTextBlock(tb);
}
return _computedFormat;
}
/** @private */
tlf_internal override function canOwnFlowElement(elem:FlowElement):Boolean
{
return elem is FlowLeafElement || elem is SubParagraphGroupElementBase || elem is TableElement;
}
/** @private */
tlf_internal override function normalizeRange(normalizeStart:uint,normalizeEnd:uint):void
{
var idx:int = findChildIndexAtPosition(normalizeStart);
if (idx != -1 && idx < numChildren)
{
var child:FlowElement = getChildAt(idx);
normalizeStart = normalizeStart-child.parentRelativeStart;
CONFIG::debug { assert(normalizeStart >= 0, "bad normalizeStart in normalizeRange"); }
for (;;)
{
// watch out for changes in the length of the child
var origChildEnd:int = child.parentRelativeStart+child.textLength;
child.normalizeRange(normalizeStart,normalizeEnd-child.parentRelativeStart);
var newChildEnd:int = child.parentRelativeStart+child.textLength;
normalizeEnd += newChildEnd-origChildEnd; // adjust
// no zero length children
if (child.textLength == 0 && !child.bindableElement)
replaceChildren(idx,idx+1);
else if (child.mergeToPreviousIfPossible())
{
var prevElement:FlowElement = this.getChildAt(idx-1);
// possibly optimize the start to the length of prevelement before the merge
prevElement.normalizeRange(0,prevElement.textLength);
}
else
idx++;
if (idx == numChildren)
{
// check if last child is an empty SubPargraphBlock and remove it
if (idx != 0)
{
var lastChild:FlowElement = this.getChildAt(idx-1);
if (lastChild is SubParagraphGroupElementBase && lastChild.textLength == 1 && !lastChild.bindableElement)
replaceChildren(idx-1,idx);
}
break;
}
// next child
child = getChildAt(idx);
if (child.parentRelativeStart > normalizeEnd)
break;
normalizeStart = 0; // for the next child
}
}
// empty paragraphs not allowed after normalize
if (numChildren == 0 || textLength == 0)
{
var s:SpanElement = new SpanElement();
replaceChildren(0,0,s);
s.normalizeRange(0,s.textLength);
}
}
/** @private */
tlf_internal function getEffectiveLeadingModel():String
{
return computedFormat.leadingModel == LeadingModel.AUTO ? LocaleUtil.leadingModel(computedFormat.locale) : computedFormat.leadingModel;
}
/** @private */
tlf_internal function getEffectiveDominantBaseline():String
{
return computedFormat.dominantBaseline == FormatValue.AUTO ? LocaleUtil.dominantBaseline(computedFormat.locale) : computedFormat.dominantBaseline;
}
/** @private */
tlf_internal function getEffectiveJustificationRule():String
{
return computedFormat.justificationRule == FormatValue.AUTO ? LocaleUtil.justificationRule(computedFormat.locale) : computedFormat.justificationRule;
}
/** @private */
tlf_internal function getEffectiveJustificationStyle():String
{
return computedFormat.justificationStyle == FormatValue.AUTO ? LocaleUtil.justificationStyle(computedFormat.locale) : computedFormat.justificationStyle;
}
/** @private */
CONFIG::debug public override function debugCheckFlowElement(depth:int = 0, extraData:String = ""):int
{
var tb:TextBlock = getTextBlock();
var rslt:int = super.debugCheckFlowElement(depth," fte:"+getDebugIdentity(tb)+" "+extraData);
// now check the character count and then the last character
if (tb)
{
var contentLength:int = tb.content && tb.content.rawText ? tb.content.rawText.length : 0;
rslt += assert(contentLength == textLength,"Bad paragraph length mode:"+textLength.toString()+" _textBlock:" + contentLength.toString());
var groupElement:GroupElement = tb.content as GroupElement;
if (groupElement)
assert(groupElement.elementCount == numChildren,"Mismatched group and elementCount");
else if (tb.content)
assert(1 == numChildren,"Mismatched group and elementCount");
else
assert(0 == numChildren,"Mismatched group and elementCount");
}
rslt += assert(numChildren == 0 || textLength > 0,"Para must have at least one text char");
return rslt;
}
/** @private */
tlf_internal static function getLeadingBasis (leadingModel:String):String
{
switch (leadingModel)
{
default:
CONFIG::debug { assert(false,"Unsupported parameter to ParagraphElement.getLeadingBasis"); } // In particular, AUTO is not supported by this method. Must be mapped to one of the above
case LeadingModel.ASCENT_DESCENT_UP:
case LeadingModel.APPROXIMATE_TEXT_FIELD:
case LeadingModel.BOX:
case LeadingModel.ROMAN_UP:
return flash.text.engine.TextBaseline.ROMAN;
case LeadingModel.IDEOGRAPHIC_TOP_UP:
case LeadingModel.IDEOGRAPHIC_TOP_DOWN:
return flash.text.engine.TextBaseline.IDEOGRAPHIC_TOP;
case LeadingModel.IDEOGRAPHIC_CENTER_UP:
case LeadingModel.IDEOGRAPHIC_CENTER_DOWN:
return flash.text.engine.TextBaseline.IDEOGRAPHIC_CENTER;
}
}
/** @private */
tlf_internal static function useUpLeadingDirection (leadingModel:String):Boolean
{
switch (leadingModel)
{
default:
CONFIG::debug { assert(false,"Unsupported parameter to ParagraphElement.useUpLeadingDirection"); } // In particular, AUTO is not supported by this method. Must be mapped to one of the above
case LeadingModel.ASCENT_DESCENT_UP:
case LeadingModel.APPROXIMATE_TEXT_FIELD:
case LeadingModel.BOX:
case LeadingModel.ROMAN_UP:
case LeadingModel.IDEOGRAPHIC_TOP_UP:
case LeadingModel.IDEOGRAPHIC_CENTER_UP:
return true;
case LeadingModel.IDEOGRAPHIC_TOP_DOWN:
case LeadingModel.IDEOGRAPHIC_CENTER_DOWN:
return false;
}
}
tlf_internal function incInteractiveChildrenCount() : void
{
++ _interactiveChildrenCount ;
}
tlf_internal function decInteractiveChildrenCount() : void
{
-- _interactiveChildrenCount ;
}
tlf_internal function hasInteractiveChildren() : Boolean
{
return _interactiveChildrenCount != 0 ;
}
tlf_internal function get terminatorSpan():SpanElement
{
return _terminatorSpan;
}
}
}