blob: 2ebb985083e37b3a9372874e8bb2d90da82c9c33 [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.utils
{
import flash.geom.Point;
import flash.geom.Rectangle;
import flash.text.engine.TextLine;
import flash.text.engine.TextRotation;
import flashx.textLayout.compose.IFlowComposer;
import flashx.textLayout.compose.TextFlowLine;
import flashx.textLayout.container.ContainerController;
import flashx.textLayout.container.ScrollPolicy;
import flashx.textLayout.elements.FlowLeafElement;
import flashx.textLayout.elements.ParagraphElement;
import flashx.textLayout.elements.TextFlow;
import flashx.textLayout.elements.TextRange;
import flashx.textLayout.formats.BlockProgression;
import flashx.textLayout.formats.Direction;
import flashx.textLayout.tlf_internal;
use namespace tlf_internal;
/**
* Utilities for manipulating a TextRange
* The methods of this class are static and must be called using
* the syntax <code>NavigationUtil.method(<em>parameter</em>)</code>.
*
* @playerversion Flash 10
* @playerversion AIR 1.5
* @langversion 3.0
*/
public final class NavigationUtil
{
private static function validateTextRange(range:TextRange):Boolean
{ return range.textFlow != null && range.anchorPosition != -1 && range.activePosition != -1; }
private static function doIncrement(flowRoot:TextFlow, pos:int, incrementer:Function):int
{
var para:ParagraphElement = flowRoot.findAbsoluteParagraph(pos);
return incrementer(flowRoot, para, pos, para.getAbsoluteStart());
}
private static function previousAtomHelper(flowRoot:TextFlow, para:ParagraphElement, pos:int, paraStart:int):int
{
if (pos - paraStart == 0)
return (pos > 0) ? pos - 1 : 0;
// mjzhang : for fix bug 2835316, will force to compose to paragraph end to avoid invalid textLine
var paraEnd:Number = para.getAbsoluteStart() + para.textLength;
para.getTextFlow().flowComposer.composeToPosition(paraEnd);
return para.findPreviousAtomBoundary(pos - paraStart) + paraStart;
}
/**
* Returns the absolute position of the previous atom.
* @playerversion Flash 10
* @playerversion AIR 1.5
* @langversion 3.0
*/
public static function previousAtomPosition(flowRoot:TextFlow, absolutePos:int):int
{
return doIncrement(flowRoot,absolutePos,previousAtomHelper);
}
private static function nextAtomHelper(flowRoot:TextFlow, para:ParagraphElement, pos:int, paraStart:int):int
{
if (pos - paraStart == para.textLength - 1)
return Math.min(flowRoot.textLength, pos + 1);
// mjzhang : for fix bug 2835316, will force to compose to paragraph end to avoid invalid textLine
var paraEnd:Number = para.getAbsoluteStart() + para.textLength;
para.getTextFlow().flowComposer.composeToPosition(paraEnd);
return para.findNextAtomBoundary(pos - paraStart) + paraStart;
}
/**
* Returns the absolute position of the next atom.
* @playerversion Flash 10
* @playerversion AIR 1.5
* @langversion 3.0
*/
public static function nextAtomPosition(flowRoot:TextFlow, absolutePos:int):int
{
return doIncrement(flowRoot,absolutePos,nextAtomHelper);
}
/**
* Returns absolute position of the beginning of the previous word.
* @playerversion Flash 10
* @playerversion AIR 1.5
* @langversion 3.0
*/
public static function previousWordPosition(flowRoot:TextFlow, absolutePos:int):int
{
if (isOverset(flowRoot, absolutePos))
return endOfLastController(flowRoot);
var para:ParagraphElement = flowRoot.findAbsoluteParagraph(absolutePos);
var paraStart:int = para.getAbsoluteStart();
// mjzhang : for fix bug 2835316, will force to compose to paragraph end to avoid invalid textLine
var paraEnd:Number = para.getAbsoluteStart() + para.textLength;
para.getTextFlow().flowComposer.composeToPosition(paraEnd);
var prevWordPos:int = absolutePos - paraStart;
var nextWordPos:int = 0;
if (absolutePos - paraStart == 0)
return (absolutePos > 0) ? absolutePos - 1 : 0;
do
{
nextWordPos = para.findPreviousWordBoundary(prevWordPos);
if (prevWordPos == nextWordPos) prevWordPos = para.findPreviousWordBoundary(prevWordPos - 1);
else prevWordPos = nextWordPos;
} while (prevWordPos > 0 && CharacterUtil.isWhitespace(para.getCharCodeAtPosition(prevWordPos)));
return prevWordPos + paraStart;
}
/**
* Returns the absolute position of the beginning of the next word.
* @playerversion Flash 10
* @playerversion AIR 1.5
* @langversion 3.0
*/
public static function nextWordPosition(flowRoot:TextFlow, absolutePos:int):int
{
var para:ParagraphElement = flowRoot.findAbsoluteParagraph(absolutePos);
var paraStart:int = para.getAbsoluteStart();
// mjzhang : for fix bug 2835316, will force to compose to paragraph end to avoid invalid textLine
var paraEnd:Number = para.getAbsoluteStart() + para.textLength;
para.getTextFlow().flowComposer.composeToPosition(paraEnd);
var nextWordPos:int = absolutePos - paraStart;
if (absolutePos - paraStart == para.textLength - 1)
return Math.min(flowRoot.textLength, absolutePos + 1);
do
{
nextWordPos = para.findNextWordBoundary(nextWordPos);
} while (nextWordPos < (para.textLength - 1) && CharacterUtil.isWhitespace(para.getCharCodeAtPosition(nextWordPos)))
return nextWordPos + paraStart;
}
/** @private */
static tlf_internal function updateStartIfInReadOnlyElement(textFlow:TextFlow, idx:int):int
{
return idx;
}
/** @private */
static tlf_internal function updateEndIfInReadOnlyElement(textFlow:TextFlow, idx:int):int
{
return idx;
}
static private function moveForwardHelper(range:TextRange, extendSelection:Boolean, incrementor:Function):Boolean
{
var textFlow:TextFlow = range.textFlow;
var begIdx:int = range.anchorPosition;
var endIdx:int = range.activePosition;
if (extendSelection)
endIdx = incrementor(textFlow, endIdx);
else {
if (begIdx == endIdx) //no selection, just move insertion point
{
begIdx = incrementor(textFlow, begIdx);
endIdx = begIdx;
}
else if (endIdx > begIdx) //change selection to insertion point
begIdx = endIdx;
else
endIdx = begIdx;
}
if (begIdx == endIdx)
{
begIdx = updateStartIfInReadOnlyElement(textFlow, begIdx);
endIdx = updateEndIfInReadOnlyElement(textFlow, endIdx);
} else {
endIdx = updateEndIfInReadOnlyElement(textFlow, endIdx);
}
if (!extendSelection && (range.anchorPosition == begIdx) && (range.activePosition == endIdx))
{
if (begIdx < endIdx)
{
begIdx = Math.min(endIdx + 1, textFlow.textLength - 1);
endIdx = begIdx;
}else {
endIdx = Math.min(begIdx + 1, textFlow.textLength - 1);
begIdx = endIdx;
}
}
return range.updateRange(begIdx,endIdx);
}
static private function moveBackwardHelper(range:TextRange, extendSelection:Boolean, incrementor:Function):Boolean
{
var textFlow:TextFlow = range.textFlow;
var begIdx:int = range.anchorPosition;
var endIdx:int = range.activePosition;
if (extendSelection) //shift key is pressed
endIdx = incrementor(textFlow, endIdx);
else {
if (begIdx == endIdx) //no selection, just move insertion point
{
begIdx = incrementor(textFlow, begIdx);
endIdx = begIdx;
}
else if (endIdx > begIdx) //change selection to insertion point
endIdx = begIdx;
else
begIdx = endIdx;
}
if (begIdx == endIdx)
{
begIdx = updateEndIfInReadOnlyElement(textFlow, begIdx);
endIdx = updateStartIfInReadOnlyElement(textFlow, endIdx);
} else {
endIdx = updateStartIfInReadOnlyElement(textFlow, endIdx);
}
if (!extendSelection && (range.anchorPosition == begIdx) && (range.activePosition == endIdx))
{
if (begIdx < endIdx)
{
endIdx = Math.max(begIdx - 1, 0);
begIdx = endIdx;
}else {
begIdx = Math.max(endIdx - 1, 0);
endIdx = begIdx;
}
}
return range.updateRange(begIdx,endIdx);
}
/**
* Sets the TextRange forward by one character.
* @param extendSelection Indicates that only activeIndex should move
* @return true if selection changed.
* @playerversion Flash 10
* @playerversion AIR 1.5
* @langversion 3.0
*/
static public function nextCharacter(range:TextRange, extendSelection:Boolean = false):Boolean
{
if (validateTextRange(range))
{
if (!adjustForOversetForward(range))
moveForwardHelper(range, extendSelection, nextAtomPosition);
return true;
}
return false;
}
/**
* Sets the TextRange backward by one character.
* @param extendSelection Indicates that only activeIndex should move
* @return true if selection changed.
* @playerversion Flash 10
* @playerversion AIR 1.5
* @langversion 3.0
*/
static public function previousCharacter(range:TextRange, extendSelection:Boolean = false):Boolean
{
if (validateTextRange(range))
{
if (!adjustForOversetBack(range))
moveBackwardHelper(range, extendSelection, previousAtomPosition);
return true;
}
return false;
}
/**
* Sets the TextRange forward by one word.
* @param extendSelection Indicates that only activeIndex should move
* @return true if selection changed.
* @playerversion Flash 10
* @playerversion AIR 1.5
* @langversion 3.0
*/
static public function nextWord(range:TextRange, extendSelection:Boolean = false):Boolean
{
if (validateTextRange(range))
{
if (!adjustForOversetForward(range))
moveForwardHelper(range, extendSelection, nextWordPosition);
return true;
}
return false;
}
/**
* Sets the TextRange backward by one word.
* @param extendSelection Indicates that only activeIndex should move
* @return true if selection changed.
* @playerversion Flash 10
* @playerversion AIR 1.5
* @langversion 3.0
*/
static public function previousWord(range:TextRange, extendSelection:Boolean = false):Boolean
{
if (validateTextRange(range))
{
if (!adjustForOversetBack(range))
moveBackwardHelper(range, extendSelection, previousWordPosition);
return true;
}
return false;
}
/** @private */
static tlf_internal function computeEndIdx(targetFlowLine:TextFlowLine,curTextFlowLine:TextFlowLine,blockProgression:String,isRTLDirection:Boolean,globalPoint:Point):int
{
var endIdx:int;
var targetTextLine:TextLine = targetFlowLine.getTextLine(true);
var currentTextLine:TextLine = curTextFlowLine.getTextLine(true);
var bidiRightToLeft:Boolean = ((currentTextLine.getAtomBidiLevel(atomIndex) % 2) != 0);
if (targetFlowLine.controller == curTextFlowLine.controller)
{
if(blockProgression != BlockProgression.RL)
{
globalPoint.y -= (currentTextLine.y - targetTextLine.y);
} else {
globalPoint.x += (targetTextLine.x - currentTextLine.x);
}
} else {
var firstAtomRect:Rectangle = targetTextLine.getAtomBounds(0);
var firstAtomPoint:Point = new Point();
firstAtomPoint.x = firstAtomRect.left;
firstAtomPoint.y = 0;
firstAtomPoint = targetTextLine.localToGlobal(firstAtomPoint);
if(blockProgression != BlockProgression.RL)
{
globalPoint.x -= curTextFlowLine.controller.container.x;
globalPoint.y = firstAtomPoint.y;
} else {
globalPoint.x = firstAtomPoint.x;
globalPoint.y -= curTextFlowLine.controller.container.y;
}
}
var atomIndex:int = targetTextLine.getAtomIndexAtPoint(globalPoint.x,globalPoint.y);
if (atomIndex == -1)
{
if(blockProgression != BlockProgression.RL) {
if (!bidiRightToLeft)
endIdx = (globalPoint.x <= targetTextLine.x) ? targetFlowLine.absoluteStart : (targetFlowLine.absoluteStart + targetFlowLine.textLength - 1);
else
endIdx = (globalPoint.x <= targetTextLine.x) ? (targetFlowLine.absoluteStart + targetFlowLine.textLength - 1) : targetFlowLine.absoluteStart;
} else {
if (!bidiRightToLeft)
endIdx = (globalPoint.y <= targetTextLine.y) ? targetFlowLine.absoluteStart : (targetFlowLine.absoluteStart + targetFlowLine.textLength - 1);
else
endIdx = (globalPoint.y <= targetTextLine.y) ? (targetFlowLine.absoluteStart + targetFlowLine.textLength - 1) : targetFlowLine.absoluteStart;
}
}
else {
// get the character box and if check we are past the middle select past this character.
var glyphRect:Rectangle = targetTextLine.getAtomBounds(atomIndex);
var leanRight:Boolean = false;
if(glyphRect)
{
//if this is TTB and NOT TCY determine lean based on Y coordinates...
var glyphGlobalPoint:Point = new Point();
glyphGlobalPoint.x = glyphRect.x;
glyphGlobalPoint.y = glyphRect.y;
glyphGlobalPoint = targetTextLine.localToGlobal(glyphGlobalPoint);
if((blockProgression == BlockProgression.RL) && targetTextLine.getAtomTextRotation(atomIndex) != TextRotation.ROTATE_0)
leanRight = (globalPoint.y > (glyphGlobalPoint.y + glyphRect.height/2));
else //use X..
leanRight = (globalPoint.x > (glyphGlobalPoint.x + glyphRect.width/2));
}
var paraSelectionIdx:int;
if ((targetTextLine.getAtomBidiLevel(atomIndex) % 2) != 0) // Right to left case, right is "start" unicode
paraSelectionIdx = leanRight ? targetTextLine.getAtomTextBlockBeginIndex(atomIndex) : targetTextLine.getAtomTextBlockEndIndex(atomIndex);
else {// Left to right case, right is "end" unicode
if (isRTLDirection) {
if ((leanRight == false) && (atomIndex > 0))
{
paraSelectionIdx = targetTextLine.getAtomTextBlockBeginIndex(atomIndex - 1);
} else {
paraSelectionIdx = targetTextLine.getAtomTextBlockBeginIndex(atomIndex);
}
} else {
paraSelectionIdx = leanRight ? targetTextLine.getAtomTextBlockEndIndex(atomIndex) : targetTextLine.getAtomTextBlockBeginIndex(atomIndex);
}
}
endIdx = targetFlowLine.paragraph.getAbsoluteStart() + paraSelectionIdx;
}
return endIdx;
}
/**
* Sets the TextRange down one line
* @param extendSelection Indicates that only activeIndex should move
* @return true if selection changed.
* @playerversion Flash 10
* @playerversion AIR 1.5
* @langversion 3.0
*/
static public function nextLine(range:TextRange, extendSelection:Boolean = false):Boolean
{
if (!validateTextRange(range))
return false;
if (adjustForOversetForward(range))
return true;
var textFlow:TextFlow = range.textFlow;
var blockProgression:String = textFlow.computedFormat.blockProgression;
var begIdx:int = range.anchorPosition;
var endIdx:int = range.activePosition;
var limitIdx:int = endOfLastController(textFlow);
var curLine:int = textFlow.flowComposer.findLineIndexAtPosition(endIdx);
var isRTLDirection:Boolean = (textFlow.computedFormat.direction == Direction.RTL);
if (curLine < textFlow.flowComposer.numLines-1) //create, expand or shrink the selection
{
var curTextFlowLine:TextFlowLine = textFlow.flowComposer.getLineAt(curLine);
var lineStart:int = curTextFlowLine.absoluteStart;
var lineDelta:int = endIdx - lineStart;
var currentTextLine:TextLine = curTextFlowLine.getTextLine(true);
var para:ParagraphElement = curTextFlowLine.paragraph;
var atomIndex:int = currentTextLine.getAtomIndexAtCharIndex(endIdx - para.getAbsoluteStart());
var bidiRightToLeft:Boolean = ((currentTextLine.getAtomBidiLevel(atomIndex) % 2) != 0);
var curPosRect:Rectangle = currentTextLine.getAtomBounds(atomIndex);
var currentTextLineX:Number = currentTextLine.x;
var curPosRectLeft:Number = curPosRect.left;
var curPosRectRight:Number = curPosRect.right;
if(blockProgression == BlockProgression.RL)
{
currentTextLineX = currentTextLine.y;
curPosRectLeft = curPosRect.top;
curPosRectRight = curPosRect.bottom;
}
//find the atom
var globalPoint:Point = new Point();
if(blockProgression != BlockProgression.RL)
{
if (!isRTLDirection)
globalPoint.x = curPosRect.left;
else
globalPoint.x = curPosRect.right;
globalPoint.y = 0;
} else {
globalPoint.x = 0;
if (!isRTLDirection)
globalPoint.y = curPosRect.top;
else
globalPoint.y = curPosRect.bottom;
}
globalPoint = currentTextLine.localToGlobal(globalPoint);
//at this point, we have the global point of our current position. Now adjust x or y to the
//baseline of the next line.
var nextFlowLine:TextFlowLine = textFlow.flowComposer.getLineAt(curLine + 1);
if (nextFlowLine.absoluteStart >= limitIdx)
{
if (!extendSelection)
range.activePosition = range.anchorPosition = textFlow.textLength - 1;
else
range.activePosition = textFlow.textLength;
return true;
}
// get the last container so that we can make sure the previous line is in view.
var controller:ContainerController = textFlow.flowComposer.getControllerAt(textFlow.flowComposer.numControllers-1);
var firstPosInContainer:int = controller.absoluteStart;
var lastPosInContainer:int = firstPosInContainer + controller.textLength;
if ((nextFlowLine.absoluteStart >= firstPosInContainer) && (nextFlowLine.absoluteStart < lastPosInContainer))
{
if (nextFlowLine.isDamaged())
{
textFlow.flowComposer.composeToPosition(nextFlowLine.absoluteStart+1);
nextFlowLine = textFlow.flowComposer.getLineAt(curLine + 1);
if (nextFlowLine.isDamaged())
return false;
}
// Scroll down one line, but allow scrolling only in the block progression direction
var curLogicalHorizontalScrollPos:Number = (blockProgression == BlockProgression.TB) ? controller.horizontalScrollPosition : controller.verticalScrollPosition;
controller.scrollToRange(nextFlowLine.absoluteStart, nextFlowLine.absoluteStart + nextFlowLine.textLength - 1);
if (blockProgression == BlockProgression.TB)
controller.horizontalScrollPosition = curLogicalHorizontalScrollPos;
else
controller.verticalScrollPosition = curLogicalHorizontalScrollPos;
}
endIdx = computeEndIdx(nextFlowLine,curTextFlowLine,blockProgression,isRTLDirection,globalPoint);
if (endIdx >= textFlow.textLength)
endIdx = textFlow.textLength;
}
else
endIdx = textFlow.textLength;
if (!extendSelection)
begIdx = endIdx;
if (begIdx == endIdx)
{
begIdx = updateStartIfInReadOnlyElement(textFlow, begIdx);
endIdx = updateEndIfInReadOnlyElement(textFlow, endIdx);
} else {
endIdx = updateEndIfInReadOnlyElement(textFlow, endIdx);
}
return range.updateRange(begIdx,endIdx);
}
/**
* Sets the TextRange up one line.
* @param extendSelection Indicates that only activeIndex should move
* @return true if selection changed.
* @playerversion Flash 10
* @playerversion AIR 1.5
* @langversion 3.0
*/
static public function previousLine(range:TextRange, extendSelection:Boolean = false):Boolean
{
if (!validateTextRange(range))
return false;
if (adjustForOversetBack(range))
return true;
var textFlow:TextFlow = range.textFlow;
var blockProgression:String = textFlow.computedFormat.blockProgression;
var begIdx:int = range.anchorPosition;
var endIdx:int = range.activePosition;
var curLine:int = textFlow.flowComposer.findLineIndexAtPosition(endIdx);
var isRTLDirection:Boolean = (textFlow.computedFormat.direction == Direction.RTL);
if (curLine > 0) //create, expand or shrink the selection
{
var curTextFlowLine:TextFlowLine = textFlow.flowComposer.getLineAt(curLine);
var lineStart:int = curTextFlowLine.absoluteStart;
var lineDelta:int = endIdx - lineStart;
var currentTextLine:TextLine = curTextFlowLine.getTextLine(true);
var para:ParagraphElement = curTextFlowLine.paragraph;
var atomIndex:int = currentTextLine.getAtomIndexAtCharIndex(endIdx - para.getAbsoluteStart());
var curPosRect:Rectangle = currentTextLine.getAtomBounds(atomIndex);
var currentTextLineX:Number = currentTextLine.x;
var curPosRectLeft:Number = curPosRect.left;
var curPosRectRight:Number = curPosRect.right;
if(blockProgression == BlockProgression.RL)
{
currentTextLineX = currentTextLine.y;
curPosRectLeft = curPosRect.top;
curPosRectRight = curPosRect.bottom;
}
//find the atom
var globalPoint:Point = new Point();
if(blockProgression != BlockProgression.RL)
{
if (!isRTLDirection)
globalPoint.x = curPosRect.left;
else
globalPoint.x = curPosRect.right;
globalPoint.y = 0;
} else {
globalPoint.x = 0;
if (!isRTLDirection)
globalPoint.y = curPosRect.top;
else
globalPoint.y = curPosRect.bottom;
}
globalPoint = currentTextLine.localToGlobal(globalPoint);
//at this point, we have the global point of our current position. Now adjust x or y to the
//baseline of the next line.
var prevFlowLine:TextFlowLine = textFlow.flowComposer.getLineAt(curLine - 1);
// get the last container so that we can make sure the previous line is in view.
var controller:ContainerController = textFlow.flowComposer.getControllerAt(textFlow.flowComposer.numControllers-1);
var firstPosInContainer:int = controller.absoluteStart;
var lastPosInContainer:int = firstPosInContainer + controller.textLength;
if ((prevFlowLine.absoluteStart >= firstPosInContainer) && (prevFlowLine.absoluteStart < lastPosInContainer))
{
// Scroll up one line, but allow scrolling only in the block progression direction
var curLogicalHorizontalScrollPos:Number = (blockProgression == BlockProgression.TB) ? controller.horizontalScrollPosition : controller.verticalScrollPosition;
controller.scrollToRange(prevFlowLine.absoluteStart,prevFlowLine.absoluteStart+prevFlowLine.textLength-1);
if (blockProgression == BlockProgression.TB)
controller.horizontalScrollPosition = curLogicalHorizontalScrollPos;
else
controller.verticalScrollPosition = curLogicalHorizontalScrollPos;
}
endIdx = computeEndIdx(prevFlowLine,curTextFlowLine,blockProgression,isRTLDirection,globalPoint);
}
else
{
endIdx = 0;
}
if (!extendSelection)
begIdx = endIdx;
if (begIdx == endIdx)
{
begIdx = updateStartIfInReadOnlyElement(textFlow, begIdx);
endIdx = updateEndIfInReadOnlyElement(textFlow, endIdx);
} else {
endIdx = updateEndIfInReadOnlyElement(textFlow, endIdx);
}
return range.updateRange(begIdx,endIdx);
}
/**
* Sets the TextRange down one page.
* @param extendSelection Indicates that only activeIndex should move
* @return true if selection changed.
* @playerversion Flash 10
* @playerversion AIR 1.5
* @langversion 3.0
*/
static public function nextPage(range:TextRange, extendSelection:Boolean = false):Boolean
{
var controller:ContainerController;
if (!validateTextRange(range))
return false;
var textFlow:TextFlow = range.textFlow;
// if not the last container go to the beginning of the next container
var controllerIndex:int = textFlow.flowComposer.findControllerIndexAtPosition(range.activePosition);
if (controllerIndex != textFlow.flowComposer.numControllers-1)
{
range.activePosition = textFlow.flowComposer.getControllerAt(controllerIndex+1).absoluteStart;
if (!extendSelection)
range.anchorPosition = range.activePosition;
return true;
}
if (!isScrollable(textFlow, range.activePosition)) // paging applies only to containers that scroll
return false;
if (adjustForOversetForward(range))
return true;
var begIdx:int = range.absoluteStart;
var endIdx:int = range.absoluteEnd;
var curLine:int = textFlow.flowComposer.findLineIndexAtPosition(endIdx);
var curTextFlowLine:TextFlowLine = textFlow.flowComposer.getLineAt(curLine);
var lineStart:int = textFlow.flowComposer.getLineAt(curLine).absoluteStart;
var linePos:int = endIdx - lineStart;
var nextLine:int;
var nextTextFlowLine:TextFlowLine = curTextFlowLine;
var isTTB:Boolean = textFlow.computedFormat.blockProgression == BlockProgression.RL;
var amount:Number;
// get the last container
controller = textFlow.flowComposer.getControllerAt(textFlow.flowComposer.numControllers-1);
if (isTTB)
{
amount = controller.compositionWidth * textFlow.configuration.scrollPagePercentage;
} else {
amount = controller.compositionHeight * textFlow.configuration.scrollPagePercentage;
}
if (isTTB)
{
var contentWidth:Number = controller.contentWidth;
if ((controller.horizontalScrollPosition - amount) < -contentWidth)
{
controller.horizontalScrollPosition = -contentWidth;
nextLine = textFlow.flowComposer.numLines - 1;
nextTextFlowLine = textFlow.flowComposer.getLineAt(nextLine);
} else
{
var oldHorzScrollPos:Number = controller.horizontalScrollPosition;
controller.horizontalScrollPosition -= amount;
var newHorzScrollPos:Number = controller.horizontalScrollPosition;
if (oldHorzScrollPos == newHorzScrollPos) {
nextLine = textFlow.flowComposer.numLines - 1;
nextTextFlowLine = textFlow.flowComposer.getLineAt(nextLine);
} else {
nextLine = curLine;
while (nextLine < (textFlow.flowComposer.numLines - 1))
{
nextLine++;
nextTextFlowLine = textFlow.flowComposer.getLineAt(nextLine);
if ((curTextFlowLine.x - nextTextFlowLine.x) >= (oldHorzScrollPos - newHorzScrollPos))
break;
}
}
}
}
else
{
var contentHeight:Number = controller.contentHeight;
if ((controller.verticalScrollPosition + amount) > contentHeight)
{
controller.verticalScrollPosition = contentHeight;
nextLine = textFlow.flowComposer.numLines - 1;
nextTextFlowLine = textFlow.flowComposer.getLineAt(nextLine);
} else
{
var oldVertScrollPos:Number = controller.verticalScrollPosition;
controller.verticalScrollPosition += amount;
var newVertScrollPos:Number = controller.verticalScrollPosition;
if (newVertScrollPos == oldVertScrollPos) {
nextLine = textFlow.flowComposer.numLines - 1;
nextTextFlowLine = textFlow.flowComposer.getLineAt(nextLine);
} else {
nextLine = curLine;
while (nextLine < (textFlow.flowComposer.numLines - 1))
{
nextLine++;
nextTextFlowLine = textFlow.flowComposer.getLineAt(nextLine);
if ((nextTextFlowLine.y - curTextFlowLine.y) >= (newVertScrollPos - oldVertScrollPos))
break;
}
}
}
}
endIdx = nextTextFlowLine.absoluteStart + linePos;
var nextLineEnd:int = nextTextFlowLine.absoluteStart + nextTextFlowLine.textLength - 1;
if (endIdx > nextLineEnd)
{
endIdx = nextLineEnd;
}
if (!extendSelection)
begIdx = endIdx;
if (begIdx == endIdx)
{
begIdx = updateEndIfInReadOnlyElement(textFlow, begIdx);
endIdx = updateStartIfInReadOnlyElement(textFlow, endIdx);
} else {
endIdx = updateStartIfInReadOnlyElement(textFlow, endIdx);
}
return range.updateRange(begIdx,endIdx);
}
/**
* Sets the TextRange up one page.
* @param extendSelection Indicates that only activeIndex should move
* @return true if selection changed.
* @playerversion Flash 10
* @playerversion AIR 1.5
* @langversion 3.0
*/
static public function previousPage(range:TextRange, extendSelection:Boolean = false):Boolean
{
if (!validateTextRange(range))
return false;
var textFlow:TextFlow = range.textFlow;
var controllerIndex:int = textFlow.flowComposer.findControllerIndexAtPosition(range.activePosition);
var controller:ContainerController = textFlow.flowComposer.getControllerAt(controllerIndex);
// first line in container
var controllerFirstLine:TextFlowLine = textFlow.flowComposer.findLineAtPosition(controller.absoluteStart);
// if on the first line of a controller go to the beginning of the previous controller
if (range.activePosition <= controller.absoluteStart+controllerFirstLine.textLength)
{
if (controllerIndex == 0)
return false;
range.activePosition = textFlow.flowComposer.getControllerAt(controllerIndex-1).absoluteStart;
if (!extendSelection)
range.anchorPosition = range.activePosition;
return true;
}
// if not the last container go to the beginning of the current container
if (controllerIndex != textFlow.flowComposer.numControllers-1)
{
range.activePosition = controller.absoluteStart;
if (!extendSelection)
range.anchorPosition = range.activePosition;
return true;
}
if (!isScrollable(textFlow, range.activePosition)) // paging applies only to containers that scroll
return false;
if (adjustForOversetBack(range))
return true;
var begIdx:int = range.absoluteStart;
var endIdx:int = range.absoluteEnd;
var curLine:int = textFlow.flowComposer.findLineIndexAtPosition(endIdx);
var curTextFlowLine:TextFlowLine = textFlow.flowComposer.getLineAt(curLine);
var lineStart:int = textFlow.flowComposer.getLineAt(curLine).absoluteStart;
var linePos:int = endIdx - lineStart;
var nextLine:int;
var nextTextFlowLine:TextFlowLine = curTextFlowLine;
var isTTB:Boolean = textFlow.computedFormat.blockProgression == BlockProgression.RL;
var amount:Number;
// get the last container
controller = textFlow.flowComposer.getControllerAt(textFlow.flowComposer.numControllers-1);
if (isTTB)
{
amount = controller.compositionWidth * textFlow.configuration.scrollPagePercentage;
} else {
amount = controller.compositionHeight * textFlow.configuration.scrollPagePercentage;
}
if (isTTB)
{
if ((controller.horizontalScrollPosition + amount + controller.compositionWidth) > 0)
{
controller.horizontalScrollPosition = 0;
nextLine = textFlow.flowComposer.findLineIndexAtPosition(controller.absoluteStart);
nextTextFlowLine = textFlow.flowComposer.getLineAt(nextLine);
} else
{
var oldHorzPos:Number = controller.horizontalScrollPosition;
controller.horizontalScrollPosition += amount;
var newHorzPos:Number = controller.horizontalScrollPosition;
if (oldHorzPos == newHorzPos) {
nextLine = textFlow.flowComposer.findLineIndexAtPosition(controller.absoluteStart);
nextTextFlowLine = textFlow.flowComposer.getLineAt(nextLine);
} else {
nextLine = curLine;
while (nextLine > 0)
{
nextLine--;
nextTextFlowLine = textFlow.flowComposer.getLineAt(nextLine);
if ((nextTextFlowLine.x - curTextFlowLine.x) >= (newHorzPos - oldHorzPos) || nextTextFlowLine.absoluteStart < controller.absoluteStart)
break;
}
}
}
}
else
{
if ((controller.verticalScrollPosition - amount + controller.compositionHeight) < 0)
{
controller.verticalScrollPosition = 0;
nextLine = textFlow.flowComposer.findLineIndexAtPosition(controller.absoluteStart);
nextTextFlowLine = textFlow.flowComposer.getLineAt(nextLine);
} else
{
var oldVertPos:Number = controller.verticalScrollPosition;
controller.verticalScrollPosition -= amount;
var newVertPos:Number = controller.verticalScrollPosition;
if (oldVertPos == newVertPos) {
nextLine = textFlow.flowComposer.findLineIndexAtPosition(controller.absoluteStart);
nextTextFlowLine = textFlow.flowComposer.getLineAt(nextLine);
} else {
nextLine = curLine;
while (nextLine > 0)
{
nextLine--;
nextTextFlowLine = textFlow.flowComposer.getLineAt(nextLine);
if ((curTextFlowLine.y - nextTextFlowLine.y) >= (oldVertPos - newVertPos) || nextTextFlowLine.absoluteStart < controller.absoluteStart)
break;
}
}
}
}
endIdx = nextTextFlowLine.absoluteStart + linePos;
var nextLineEnd:int = nextTextFlowLine.absoluteStart + nextTextFlowLine.textLength - 1;
if (endIdx > nextLineEnd)
{
endIdx = nextLineEnd;
}
if (!extendSelection)
begIdx = endIdx;
if (begIdx == endIdx)
{
begIdx = updateEndIfInReadOnlyElement(textFlow, begIdx);
endIdx = updateStartIfInReadOnlyElement(textFlow, endIdx);
} else {
endIdx = updateStartIfInReadOnlyElement(textFlow, endIdx);
}
return range.updateRange(begIdx,endIdx);
}
/**
* Sets the TextRange at the end of the line.
* @param extendSelection Indicates that only activeIndex should move
* @return true if selection changed.
* @playerversion Flash 10
* @playerversion AIR 1.5
* @langversion 3.0
*/
static public function endOfLine(range:TextRange, extendSelection:Boolean = false):Boolean
{
if (!validateTextRange(range))
return false;
var textFlow:TextFlow = range.textFlow;
checkCompose(textFlow.flowComposer, range.absoluteEnd);
var begIdx:int = range.anchorPosition;
var endIdx:int = range.activePosition;
var curLine:int = textFlow.flowComposer.findLineIndexAtPosition(endIdx);
var lineStart:int = textFlow.flowComposer.getLineAt(curLine).absoluteStart;
var lineEnd:int = lineStart + textFlow.flowComposer.getLineAt(curLine).textLength - 1;
var leaf:FlowLeafElement = textFlow.findLeaf(endIdx);
var para:ParagraphElement = leaf.getParagraph();
if (CharacterUtil.isWhitespace(para.getCharCodeAtPosition(lineEnd - para.getAbsoluteStart())))
{
endIdx = lineEnd;
} else {
endIdx = lineEnd + 1;
}
if (!extendSelection)
begIdx = endIdx;
if (begIdx == endIdx)
{
begIdx = updateEndIfInReadOnlyElement(textFlow, begIdx);
endIdx = updateStartIfInReadOnlyElement(textFlow, endIdx);
} else {
endIdx = updateStartIfInReadOnlyElement(textFlow, endIdx);
}
return range.updateRange(begIdx,endIdx);
}
/**
* Sets the TextRange at the beginning of the line.
* @param extendSelection Indicates that only activeIndex should move
* @return true if selection changed.
* @playerversion Flash 10
* @playerversion AIR 1.5
* @langversion 3.0
*/
static public function startOfLine(range:TextRange, extendSelection:Boolean = false):Boolean
{
if (!validateTextRange(range))
return false;
var textFlow:TextFlow = range.textFlow;
checkCompose(textFlow.flowComposer, range.absoluteEnd);
var begIdx:int = range.anchorPosition;
var endIdx:int = range.activePosition;
var curLine:int = textFlow.flowComposer.findLineIndexAtPosition(endIdx);
var lineStart:int = textFlow.flowComposer.getLineAt(curLine).absoluteStart;
endIdx = lineStart;
if (!extendSelection)
begIdx = endIdx;
if (begIdx == endIdx)
{
begIdx = updateEndIfInReadOnlyElement(textFlow, begIdx);
endIdx = updateStartIfInReadOnlyElement(textFlow, endIdx);
} else {
endIdx = updateStartIfInReadOnlyElement(textFlow, endIdx);
}
return range.updateRange(begIdx,endIdx);
}
/**
* Sets the TextRange at the end of the document.
* @param extendSelection Indicates that only activeIndex should move
* @return true if selection changed.
* @playerversion Flash 10
* @playerversion AIR 1.5
* @langversion 3.0
*/
static public function endOfDocument(range:TextRange, extendSelection:Boolean = false):Boolean
{
if (!validateTextRange(range))
return false;
var textFlow:TextFlow = range.textFlow
var begIdx:int = range.anchorPosition;
var endIdx:int = range.activePosition;
endIdx = textFlow.textLength;
if (!extendSelection)
begIdx = endIdx;
if (begIdx == endIdx)
{
begIdx = updateEndIfInReadOnlyElement(textFlow, begIdx);
endIdx = updateStartIfInReadOnlyElement(textFlow, endIdx);
} else {
endIdx = updateStartIfInReadOnlyElement(textFlow, endIdx);
}
return range.updateRange(begIdx,endIdx);
}
/**
* Sets the TextRange at the beginning of the document.
* @param extendSelection Indicates that only activeIndex should move
* @return true if selection changed.
* @playerversion Flash 10
* @playerversion AIR 1.5
* @langversion 3.0
*/
static public function startOfDocument(range:TextRange, extendSelection:Boolean = false):Boolean
{
var begIdx:int = range.anchorPosition;
var endIdx:int = 0;
if (!extendSelection)
begIdx = endIdx;
if (begIdx == endIdx)
{
begIdx = updateEndIfInReadOnlyElement(range.textFlow, begIdx);
endIdx = updateStartIfInReadOnlyElement(range.textFlow, endIdx);
} else {
endIdx = updateStartIfInReadOnlyElement(range.textFlow, endIdx);
}
return range.updateRange(begIdx,endIdx);
}
/**
* Sets the TextRange at the beginning of the paragraph.
* @param extendSelection Indicates that only activeIndex should move
* @return true if selection changed.
* @playerversion Flash 10
* @playerversion AIR 1.5
* @langversion 3.0
*/
static public function startOfParagraph(range:TextRange, extendSelection:Boolean = false):Boolean
{
var begIdx:int = range.anchorPosition;
var endIdx:int = range.activePosition;
var leaf:FlowLeafElement = range.textFlow.findLeaf(endIdx);
var para:ParagraphElement = leaf.getParagraph();
endIdx = para.getAbsoluteStart();
if (!extendSelection)
begIdx = endIdx;
if (begIdx == endIdx)
{
begIdx = updateStartIfInReadOnlyElement(range.textFlow, begIdx);
endIdx = updateEndIfInReadOnlyElement(range.textFlow, endIdx);
} else {
endIdx = updateEndIfInReadOnlyElement(range.textFlow, endIdx);
}
return range.updateRange(begIdx,endIdx);
}
/**
* Sets the TextRange at the end of the paragraph.
* @param extendSelection Indicates that only activeIndex should move
* @return true if selection changed.
* @playerversion Flash 10
* @playerversion AIR 1.5
* @langversion 3.0
*/
static public function endOfParagraph(range:TextRange, extendSelection:Boolean = false):Boolean
{
if (!validateTextRange(range))
return false;
var begIdx:int = range.anchorPosition;
var endIdx:int = range.activePosition;
var leaf:FlowLeafElement = range.textFlow.findLeaf(endIdx);
var para:ParagraphElement = leaf.getParagraph();
endIdx = para.getAbsoluteStart() + para.textLength - 1;
if (!extendSelection)
begIdx = endIdx;
if (begIdx == endIdx)
{
begIdx = updateStartIfInReadOnlyElement(range.textFlow, begIdx);
endIdx = updateEndIfInReadOnlyElement(range.textFlow, endIdx);
} else {
endIdx = updateEndIfInReadOnlyElement(range.textFlow, endIdx);
}
return range.updateRange(begIdx,endIdx);
}
/** If the range is in overset text (after the last container in a non-scrolling flow), adjust the range so it is at the end of the flow. */
static private function adjustForOversetForward(range:TextRange):Boolean
{
var flowComposer:IFlowComposer = range.textFlow.flowComposer;
var controller:ContainerController = null;
checkCompose(flowComposer, range.absoluteEnd);
if (range.absoluteEnd >= flowComposer.damageAbsoluteStart - 1)
{
clampToFit(range, flowComposer.damageAbsoluteStart - 1);
return true;
}
if (flowComposer && flowComposer.numControllers)
{
var controllerIndex:int = flowComposer.findControllerIndexAtPosition(range.absoluteEnd);
if (controllerIndex >= 0)
controller = flowComposer.getControllerAt(controllerIndex);
if (controllerIndex == flowComposer.numControllers-1)
{
if (controller.absoluteStart + controller.textLength <= range.absoluteEnd && controller.absoluteStart + controller.textLength != range.textFlow.textLength)
controller = null;
}
}
if (!controller) // we're overset, or one position before overset
{
range.anchorPosition = range.textFlow.textLength;
range.activePosition = range.anchorPosition;
return true;
}
return false;
}
static private function clampToFit(range:TextRange, endPos:int):void
{
if (endPos < 0)
endPos = 0;
range.anchorPosition = Math.min(range.anchorPosition, endPos);
range.activePosition = Math.min(range.activePosition, endPos);
}
/** If the range is in overset text (after the last container in a non-scrolling flow), adjust the range so it is at the end of the last controller in the flow. */
static private function adjustForOversetBack(range:TextRange):Boolean
{
var flowComposer:IFlowComposer = range.textFlow.flowComposer;
if (flowComposer)
{
checkCompose(flowComposer, range.absoluteEnd);
if (range.absoluteEnd > flowComposer.damageAbsoluteStart - 1)
{
clampToFit(range, flowComposer.damageAbsoluteStart - 1);
return true;
}
if (flowComposer.findControllerIndexAtPosition(range.absoluteEnd) == -1)
{
range.anchorPosition = endOfLastController(range.textFlow);
range.activePosition = range.anchorPosition;
return true;
}
}
return false;
}
private static function checkCompose(flowComposer:IFlowComposer, pos:int):void
{
if (flowComposer.damageAbsoluteStart <= pos)
flowComposer.composeToPosition(pos);
}
// Returns absolute position of the last controller in the flow, or 0 if the flow has no controllers
private static function endOfLastController(flowRoot:TextFlow):int
{
var flowComposer:IFlowComposer = flowRoot.flowComposer;
if (!flowComposer || flowComposer.numControllers <= 0)
return 0;
var controller:ContainerController = flowComposer.getControllerAt(flowComposer.numControllers - 1);
return controller.absoluteStart + Math.max(controller.textLength - 1, 0);
}
// Returns true if the position is in the overset text after the last container in the flow.
private static function isOverset(flowRoot:TextFlow, absolutePos:int):Boolean
{
var flowComposer:IFlowComposer = flowRoot.flowComposer;
return (!flowComposer || flowComposer.findControllerIndexAtPosition(absolutePos) == -1);
}
// Returns true if the position is in a scollable container
private static function isScrollable(flowRoot:TextFlow, absolutePos:int):Boolean
{
var flowComposer:IFlowComposer = flowRoot.flowComposer;
if (!flowComposer)
return false;
var controllerIndex:int = flowComposer.findControllerIndexAtPosition(absolutePos);
if (controllerIndex >= 0)
{
var controller:ContainerController = flowComposer.getControllerAt(controllerIndex);
var blockProgression:String = controller.rootElement.computedFormat.blockProgression;
return ((blockProgression == BlockProgression.TB && controller.verticalScrollPolicy != ScrollPolicy.OFF) ||
(blockProgression == BlockProgression.RL && controller.horizontalScrollPolicy != ScrollPolicy.OFF));
}
return false;
}
}
}