//////////////////////////////////////////////////////////////////////////////// // // 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 NavigationUtil.method(parameter). * * @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; 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 - 1, pos + 1); 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(); 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(); var nextWordPos:int = absolutePos - paraStart; if (absolutePos - paraStart == para.textLength - 1) return Math.min(flowRoot.textLength - 1, 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; } /** * 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 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(textFlow.computedFormat.blockProgression == BlockProgression.RL) { currentTextLineX = currentTextLine.y; curPosRectLeft = curPosRect.top; curPosRectRight = curPosRect.bottom; } //find the atom var globalPoint:Point = new Point(); if(textFlow.computedFormat.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.anchorPosition = textFlow.textLength-1; range.activePosition = textFlow.textLength-1; 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); } controller.scrollToRange(nextFlowLine.absoluteStart, nextFlowLine.absoluteStart + nextFlowLine.textLength - 1); } var nextTextLine:TextLine = nextFlowLine.getTextLine(true); if (nextFlowLine.controller == curTextFlowLine.controller) { if(textFlow.computedFormat.blockProgression != BlockProgression.RL) { globalPoint.y += (nextFlowLine.y - curTextFlowLine.y); } else { globalPoint.x -= (curTextFlowLine.x - nextFlowLine.x); } } else { var firstAtomRect:Rectangle = nextTextLine.getAtomBounds(0); var firstAtomPoint:Point = new Point(); firstAtomPoint.x = firstAtomRect.left; firstAtomPoint.y = 0; firstAtomPoint = nextTextLine.localToGlobal(firstAtomPoint); if(textFlow.computedFormat.blockProgression != BlockProgression.RL) { globalPoint.x += nextFlowLine.controller.container.x; globalPoint.y = firstAtomPoint.y; } else { globalPoint.x = firstAtomPoint.x; globalPoint.y += nextFlowLine.controller.container.y; } } atomIndex = nextTextLine.getAtomIndexAtPoint(globalPoint.x,globalPoint.y); if (atomIndex == -1) { if(textFlow.computedFormat.blockProgression != BlockProgression.RL) { if (!bidiRightToLeft) { endIdx = (globalPoint.x <= nextTextLine.x) ? nextFlowLine.absoluteStart : (nextFlowLine.absoluteStart + nextFlowLine.textLength - 1); } else { endIdx = (globalPoint.x <= nextTextLine.x) ? (nextFlowLine.absoluteStart + nextFlowLine.textLength - 1) : nextFlowLine.absoluteStart; } } else { if (!bidiRightToLeft) { endIdx = (globalPoint.y <= nextTextLine.y) ? nextFlowLine.absoluteStart : (nextFlowLine.absoluteStart + nextFlowLine.textLength - 1); } else { endIdx = (globalPoint.y <= nextTextLine.y) ? (nextFlowLine.absoluteStart + nextFlowLine.textLength - 1) : nextFlowLine.absoluteStart; } } } else { // get the character box and if check we are past the middle select past this character. var glyphRect:Rectangle = nextTextLine.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 = nextTextLine.localToGlobal(glyphGlobalPoint); if((textFlow.computedFormat.blockProgression == BlockProgression.RL) && nextTextLine.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 ((nextTextLine.getAtomBidiLevel(atomIndex) % 2) != 0) // Right to left case, right is "start" unicode paraSelectionIdx = leanRight ? nextTextLine.getAtomTextBlockBeginIndex(atomIndex) : nextTextLine.getAtomTextBlockEndIndex(atomIndex); else {// Left to right case, right is "end" unicode if (isRTLDirection) { if ((leanRight == false) && (atomIndex > 0)) { paraSelectionIdx = nextTextLine.getAtomTextBlockBeginIndex(atomIndex - 1); } else { paraSelectionIdx = nextTextLine.getAtomTextBlockBeginIndex(atomIndex); } } else { paraSelectionIdx = leanRight ? nextTextLine.getAtomTextBlockEndIndex(atomIndex) : nextTextLine.getAtomTextBlockBeginIndex(atomIndex); } } endIdx = nextFlowLine.paragraph.getAbsoluteStart() + paraSelectionIdx; if (endIdx >= textFlow.textLength) { endIdx = textFlow.textLength - 1; } } } else { endIdx = textFlow.textLength - 1; } 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 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 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(textFlow.computedFormat.blockProgression == BlockProgression.RL) { currentTextLineX = currentTextLine.y; curPosRectLeft = curPosRect.top; curPosRectRight = curPosRect.bottom; } //find the atom var globalPoint:Point = new Point(); if(textFlow.computedFormat.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)) { controller.scrollToRange(prevFlowLine.absoluteStart,prevFlowLine.absoluteStart+prevFlowLine.textLength-1); } var preventextLine:TextLine = prevFlowLine.getTextLine(true); if (prevFlowLine.controller == curTextFlowLine.controller) { if(textFlow.computedFormat.blockProgression != BlockProgression.RL) { globalPoint.y -= (currentTextLine.y - preventextLine.y); } else { globalPoint.x += (preventextLine.x - currentTextLine.x); } } else { var firstAtomRect:Rectangle = preventextLine.getAtomBounds(0); var firstAtomPoint:Point = new Point(); firstAtomPoint.x = firstAtomRect.left; firstAtomPoint.y = 0; firstAtomPoint = preventextLine.localToGlobal(firstAtomPoint); if(textFlow.computedFormat.blockProgression != BlockProgression.RL) { globalPoint.x -= curTextFlowLine.controller.container.x; globalPoint.y = firstAtomPoint.y; } else { globalPoint.x = firstAtomPoint.x; globalPoint.y -= curTextFlowLine.controller.container.y; } } atomIndex = preventextLine.getAtomIndexAtPoint(globalPoint.x,globalPoint.y); if (atomIndex == -1) { if(textFlow.computedFormat.blockProgression != BlockProgression.RL) { if (!bidiRightToLeft) endIdx = (globalPoint.x <= preventextLine.x) ? prevFlowLine.absoluteStart : (prevFlowLine.absoluteStart + prevFlowLine.textLength - 1); else endIdx = (globalPoint.x <= preventextLine.x) ? (prevFlowLine.absoluteStart + prevFlowLine.textLength - 1) : prevFlowLine.absoluteStart; } else { if (!bidiRightToLeft) endIdx = (globalPoint.y <= preventextLine.y) ? prevFlowLine.absoluteStart : (prevFlowLine.absoluteStart + prevFlowLine.textLength - 1); else endIdx = (globalPoint.y <= preventextLine.y) ? (prevFlowLine.absoluteStart + prevFlowLine.textLength - 1) : prevFlowLine.absoluteStart; } } else { // get the character box and if check we are past the middle select past this character. var glyphRect:Rectangle = preventextLine.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 = preventextLine.localToGlobal(glyphGlobalPoint); if((textFlow.computedFormat.blockProgression == BlockProgression.RL) && preventextLine.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 ((preventextLine.getAtomBidiLevel(atomIndex) % 2) != 0) // Right to left case, right is "start" unicode paraSelectionIdx = leanRight ? preventextLine.getAtomTextBlockBeginIndex(atomIndex) : preventextLine.getAtomTextBlockEndIndex(atomIndex); else {// Left to right case, right is "end" unicode if (isRTLDirection) { if ((leanRight == false) && (atomIndex > 0)) { paraSelectionIdx = preventextLine.getAtomTextBlockBeginIndex(atomIndex - 1); } else { paraSelectionIdx = preventextLine.getAtomTextBlockBeginIndex(atomIndex); } } else { paraSelectionIdx = leanRight ? preventextLine.getAtomTextBlockEndIndex(atomIndex) : preventextLine.getAtomTextBlockBeginIndex(atomIndex); } } endIdx = prevFlowLine.paragraph.getAbsoluteStart() + paraSelectionIdx; } } 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) { if ((controller.horizontalScrollPosition - amount) < -controller.contentWidth) { controller.horizontalScrollPosition = -controller.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 { if ((controller.verticalScrollPosition + amount) > controller.contentHeight) { controller.verticalScrollPosition = controller.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 nextLinePos: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 - 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 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 (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 - 1; range.activePosition = range.anchorPosition; return true; } return false; } /** 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 (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; } } }