| //////////////////////////////////////////////////////////////////////////////// |
| // |
| // 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.compose |
| { |
| import flash.text.engine.TextBlock; |
| import flash.text.engine.TextLine; |
| import flash.text.engine.TextLineValidity; |
| |
| import flashx.textLayout.container.ContainerController; |
| import flashx.textLayout.debug.Debugging; |
| import flashx.textLayout.debug.assert; |
| import flashx.textLayout.elements.BackgroundManager; |
| import flashx.textLayout.elements.FlowLeafElement; |
| import flashx.textLayout.elements.TextFlow; |
| import flashx.textLayout.tlf_internal; |
| |
| use namespace tlf_internal; |
| |
| [Exclude(name="initializeLines",kind="method")] |
| [Exclude(name="addLine",kind="method")] |
| [Exclude(name="lines",kind="property")] |
| [Exclude(name="debugCheckTextFlowLines",kind="method")] |
| [Exclude(name="checkFirstDamage",kind="method")] |
| |
| /** |
| * The FlowComposerBase class is the base class for Text Layout Framework flow composer classes, which control the |
| * composition of text lines in ContainerController objects. |
| * |
| * <p>FlowComposerBase is a utility class that implements methods and properties that are common |
| * to several types of flow composer. Application code would not typically instantiate or use this class |
| * (unless extending it to create a custom flow composer).</p> |
| * |
| * @see flashx.textLayout.elements.TextFlow#flowComposer |
| * @playerversion Flash 10 |
| * @playerversion AIR 1.5 |
| * @langversion 3.0 |
| */ |
| |
| public class FlowComposerBase |
| { |
| // Composition data |
| [ ArrayElementType("text.elements.TextFlowLine") ] |
| private var _lines:Array; |
| |
| /** @private */ |
| protected var _textFlow:TextFlow; |
| |
| /** Absolute start of the damage area -- first character in the flow that is dirty and needs to be recomposed. @private */ |
| protected var _damageAbsoluteStart:int; |
| |
| /** @private */ |
| protected var _swfContext:ISWFContext; |
| |
| /** Constructor. |
| * |
| * @playerversion Flash 10 |
| * @playerversion AIR 1.5 |
| * @langversion 3.0 |
| */ |
| |
| public function FlowComposerBase() |
| { |
| _lines = new Array(); |
| _swfContext = null; |
| } |
| |
| /** Returns the array of lines. @private */ |
| public function get lines():Array |
| { return _lines; } |
| |
| /** |
| * @copy IFlowComposer#getLineAt() |
| * |
| * @playerversion Flash 10 |
| * @playerversion AIR 1.5 |
| * @langversion 3.0 |
| */ |
| |
| public function getLineAt(index:int):TextFlowLine |
| { return _lines[index]; } |
| |
| /** @copy IFlowComposer#numLines |
| * |
| * @playerversion Flash 10 |
| * @playerversion AIR 1.5 |
| * @langversion 3.0 |
| */ |
| |
| public function get numLines():int |
| { return _lines.length; } |
| |
| /** |
| * The TextFlow object to which this flow composer is attached. |
| * |
| * @see flashx.textLayout.elements.TextFlow |
| * |
| * @playerversion Flash 10 |
| * @playerversion AIR 1.5 |
| * @langversion 3.0 |
| */ |
| |
| public function get textFlow():TextFlow |
| { return _textFlow; } |
| |
| /** |
| * The absolute position immediately preceding the first element in the text |
| * flow that requires composition and updating. |
| * |
| * @playerversion Flash 10 |
| * @playerversion AIR 1.5 |
| * @langversion 3.0 |
| */ |
| public function get damageAbsoluteStart():int |
| { |
| return _damageAbsoluteStart; |
| } |
| |
| /** |
| * Initialize the lines for the TextFlow. Creates a single TextFlowLine with no content. @private |
| */ |
| protected function initializeLines():void |
| { |
| var backgroundManager:BackgroundManager = _textFlow ? _textFlow.backgroundManager : null; |
| // remove all the lines we have now - cache for reuse |
| if (TextLineRecycler.textLineRecyclerEnabled) |
| { |
| for each (var line:TextFlowLine in _lines) |
| { |
| var textLine:TextLine = line.peekTextLine(); |
| if (textLine && !textLine.parent) |
| { |
| // releasing all textLines so release each still connected textBlock |
| if (textLine.validity != TextLineValidity.INVALID) |
| { |
| var textBlock:TextBlock = textLine.textBlock; |
| CONFIG::debug { Debugging.traceFTECall(null,textBlock,"releaseLines",textBlock.firstLine,textBlock.lastLine); } |
| textBlock.releaseLines(textBlock.firstLine,textBlock.lastLine); |
| } |
| textLine.userData = null; |
| TextLineRecycler.addLineForReuse(textLine); |
| if (backgroundManager) |
| backgroundManager.removeLineFromCache(textLine); |
| } |
| } |
| } |
| _lines.splice(0); |
| _damageAbsoluteStart = 0; |
| |
| CONFIG::debug { checkFirstDamaged(); } |
| } |
| |
| /** Make sure that there is a TextFlowLine for all the content - even if compose has stopped early. @private */ |
| protected function finalizeLinesAfterCompose():void |
| { |
| var line:TextFlowLine; |
| if (_lines.length == 0) |
| { |
| // create a new line, with damage, that covers the entire area |
| line = new TextFlowLine(null,null); |
| line.setTextLength(textFlow.textLength); |
| _lines.push(line); |
| } |
| else |
| { |
| line = _lines[_lines.length-1]; |
| var lineEnd:int = line.absoluteStart + line.textLength; |
| if (lineEnd < textFlow.textLength) |
| { |
| line = new TextFlowLine(null,null); |
| line.setAbsoluteStart(lineEnd); |
| line.setTextLength(textFlow.textLength-lineEnd); |
| _lines.push(line); |
| } |
| } |
| } |
| |
| /** |
| * @copy IFlowComposer#updateLengths() |
| * |
| * @playerversion Flash 10 |
| * @playerversion AIR 1.5 |
| * @langversion 3.0 |
| */ |
| public function updateLengths(startPosition:int,deltaLength:int):void |
| { |
| // no lines yet - skip it |
| if (numLines == 0) |
| return; |
| |
| var line:TextFlowLine; // sratch line variable |
| var lineIdx:int = findLineIndexAtPosition(startPosition); |
| |
| var damageStart:int = int.MAX_VALUE; |
| if (deltaLength > 0) |
| { |
| if (lineIdx == _lines.length) |
| { |
| line = _lines[_lines.length-1]; |
| CONFIG::debug { assert(line.absoluteStart+line.textLength == startPosition,"updateLengths bad startIdx"); } |
| line.setTextLength(line.textLength + deltaLength); |
| } |
| else |
| { |
| line = _lines[lineIdx++]; |
| line.setTextLength(line.textLength + deltaLength); |
| } |
| damageStart = line.absoluteStart; |
| } |
| else |
| { |
| var lenToDel:int = -deltaLength; |
| var curPos:int = 0; |
| |
| while (true) |
| { |
| line = _lines[lineIdx]; |
| line.setAbsoluteStart(line.absoluteStart + lenToDel + deltaLength); |
| curPos = (startPosition > line.absoluteStart ? startPosition : line.absoluteStart); |
| |
| var lineEndIdx:int = line.absoluteStart + line.textLength; |
| var deleteChars:int = 0; |
| |
| if (curPos + lenToDel <= lineEndIdx) |
| { |
| if (curPos == line.absoluteStart) |
| deleteChars = lenToDel; //delete from begin of line to end of selection |
| else if (curPos == startPosition) |
| deleteChars = lenToDel; //delete is all included in one line |
| else |
| { |
| CONFIG::debug { assert(false, "insertText: should never happen"); } |
| } |
| } |
| else //(curPos + lenToDel > lineEndIdx) //multiline delete |
| { |
| if (curPos == line.absoluteStart) |
| deleteChars = line.textLength; //delete the whole line |
| else |
| deleteChars = lineEndIdx-curPos; //delete from middle of line to end of line |
| } |
| |
| if (curPos == line.absoluteStart && curPos + deleteChars == lineEndIdx) //the whole line is selected |
| { |
| lenToDel -= deleteChars; |
| _lines.splice(lineIdx,1); //lineIdx now points to the next line |
| } |
| else //partial line |
| { |
| if (damageStart > line.absoluteStart) |
| damageStart = line.absoluteStart; |
| line.setTextLength(line.textLength - deleteChars); |
| lenToDel -= deleteChars; |
| lineIdx++; |
| } |
| CONFIG::debug { assert(lenToDel >= 0,"updateLengths deleted too much"); } |
| if (lenToDel <= 0) |
| break; |
| } |
| } |
| |
| for ( ; lineIdx < _lines.length; lineIdx++) |
| { |
| line = _lines[lineIdx]; |
| if (deltaLength >= 0) |
| line.setAbsoluteStart(line.absoluteStart + deltaLength); |
| else |
| line.setAbsoluteStart(line.absoluteStart > -deltaLength ? line.absoluteStart+deltaLength : 0); |
| } |
| |
| if (_damageAbsoluteStart > damageStart) |
| _damageAbsoluteStart = damageStart; |
| } |
| |
| /** |
| * @copy IFlowComposer#damage() |
| * |
| * @playerversion Flash 10 |
| * @playerversion AIR 1.5 |
| * @langversion 3.0 |
| */ |
| public function damage(startPosition:int, damageLength:int, damageType:String):void |
| { |
| if (ContainerController.tlf_internal::usesDiscretionaryHyphens) |
| { |
| // damage everything from the beginning. |
| // The player tends to screw up if you start |
| // composition in the middle and there are lines above broken on |
| // hyphens. |
| damageLength += startPosition; |
| startPosition = 0; |
| } |
| |
| // find the line at damageStart |
| if (_lines.length == 0 || textFlow.textLength == 0) |
| return; |
| |
| // This case the damageStart is at the end of the text. This can happen if the last paragraph is deleted |
| if (startPosition == textFlow.textLength) |
| return; |
| |
| CONFIG::debug { assert(startPosition + damageLength <= textFlow.textLength, "Damaging past end of flow!"); } |
| |
| // Start damaging one line before the startPosition location in case some of the first "damaged" line will fit on the previous line. |
| var lineIndex:int = findLineIndexAtPosition(startPosition); |
| var leaf:FlowLeafElement = textFlow.findLeaf(startPosition); |
| if (leaf && lineIndex > 0) |
| lineIndex--; |
| |
| if (lines[lineIndex].absoluteStart < _damageAbsoluteStart) |
| _damageAbsoluteStart = _lines[lineIndex].absoluteStart; |
| |
| CONFIG::debug { assert(lineIndex < _lines.length && _lines[lineIndex].absoluteStart <= startPosition + damageLength, "Missing line"); } |
| |
| while (lineIndex < _lines.length) |
| { |
| var line:TextFlowLine = _lines[lineIndex]; |
| |
| // Changed to >= from >, as > seemed to damage too |
| // many lines when editing tables. |
| // Should verify the correctness of this. |
| if (line.absoluteStart >= startPosition+damageLength) |
| break; |
| |
| line.damage(damageType); |
| lineIndex++; |
| } |
| } |
| |
| /** |
| * @copy IFlowComposer#isDamaged() |
| * |
| * @playerversion Flash 10 |
| * @playerversion AIR 1.5 |
| * @langversion 3.0 |
| */ |
| |
| public function isDamaged(absolutePosition:int):Boolean |
| { |
| // Returns true if any text from _damageAbsoluteStart through absolutePosition needs to be recomposed |
| |
| // no lines - damaged |
| if (_lines.length == 0) |
| return true; |
| |
| CONFIG::debug { checkFirstDamaged(); } |
| |
| return _damageAbsoluteStart <= absolutePosition && _damageAbsoluteStart != textFlow.textLength; |
| } |
| |
| /** @private */ |
| CONFIG::debug public function checkFirstDamaged():void |
| { |
| // find the line at start |
| if (_lines.length == 0) |
| return; |
| |
| var lineIndex:int = findLineIndexAtPosition(0); |
| while (lineIndex < _lines.length) |
| { |
| if (_lines[lineIndex].isDamaged()) |
| { |
| // trace("is damaged"); |
| CONFIG::debug { assert(_lines[lineIndex].absoluteStart >= _damageAbsoluteStart, "_damageAbsoluteStart doesn't match actual line value"); } |
| return; |
| } |
| ++lineIndex; |
| } |
| // trace("not damaged"); |
| return; |
| } |
| /** |
| * @copy IFlowComposer#findLineIndexAtPosition() |
| * |
| * @playerversion Flash 10 |
| * @playerversion AIR 1.5 |
| * @langversion 3.0 |
| */ |
| |
| public function findLineIndexAtPosition(absolutePosition:int,preferPrevious:Boolean = false):int |
| { |
| var lo:int = 0; |
| var hi:int = _lines.length-1; |
| while (lo <= hi) |
| { |
| var mid:int = (lo+hi)/2; |
| var line:TextFlowLine = _lines[mid]; |
| if (line.absoluteStart <= absolutePosition) |
| { |
| if (preferPrevious) |
| { |
| if (line.absoluteStart + line.textLength >= absolutePosition) |
| return mid; |
| } |
| else |
| { |
| if (line.absoluteStart + line.textLength > absolutePosition) |
| return mid; |
| } |
| lo = mid+1; |
| } |
| else |
| hi = mid-1; |
| } |
| return _lines.length; |
| } |
| |
| /** |
| * @copy IFlowComposer#findLineAtPosition() |
| * |
| * @playerversion Flash 10 |
| * @playerversion AIR 1.5 |
| * @langversion 3.0 |
| */ |
| |
| public function findLineAtPosition(absolutePosition:int,preferPrevious:Boolean = false):TextFlowLine |
| { |
| return _lines[findLineIndexAtPosition(absolutePosition,preferPrevious)]; |
| } |
| |
| /** |
| * add a new line |
| * Add a new line to the list of composed lines for the frame. Lines are sorted |
| * by the start location, and each line has a span. The start of the next line |
| * has to match the start of the previous line + the span of the previous line. |
| * The last line needs to end at the end of the text. Therefore, when we add a |
| * new line, we may need to adjust the span and/or start locations of other lines |
| * in the text. |
| * @private |
| */ |
| public function addLine(newLine:TextFlowLine,workIndex:int):void |
| { |
| CONFIG::debug { assert(workIndex == findLineIndexAtPosition(newLine.absoluteStart),"bad workIndex to TextFlow.addLine"); } |
| CONFIG::debug { assert (!newLine.isDamaged(), "adding damaged line"); } |
| var workLine:TextFlowLine = _lines[workIndex]; |
| var afterLine:TextFlowLine; |
| var damageStart:int = int.MAX_VALUE; |
| if (_damageAbsoluteStart == newLine.absoluteStart) |
| _damageAbsoluteStart = newLine.absoluteStart + newLine.textLength; |
| |
| if (workLine == null) |
| lines.push(newLine); |
| else if (workLine.absoluteStart != newLine.absoluteStart) |
| { |
| if (workLine.absoluteStart + workLine.textLength > newLine.absoluteStart + newLine.textLength) |
| { |
| // Making a new line in the middle of an old one. Need to split the old one. |
| afterLine = new TextFlowLine(null,newLine.paragraph); |
| afterLine.setAbsoluteStart(newLine.absoluteStart + newLine.textLength); |
| var oldCharCount:int = workLine.textLength; |
| workLine.setTextLength(newLine.absoluteStart - workLine.absoluteStart); |
| CONFIG::debug { assert(workLine.textLength != 0, "0 width line"); } |
| afterLine.setTextLength((oldCharCount - newLine.textLength) - workLine.textLength); |
| CONFIG::debug { assert(afterLine.textLength != 0, "0 width line"); } |
| _lines.splice(workIndex + 1, 0, newLine, afterLine); |
| } |
| else |
| { |
| // We're composing ahead, so we need to split the line where we're at |
| // This can happen if a table is getting composed, some cells can be composed before |
| // others that go before. |
| CONFIG::debug { assert(workLine.isDamaged(), "Uneven line boundary, but lines marked up to date"); } |
| workLine.setTextLength(newLine.absoluteStart - workLine.absoluteStart); |
| CONFIG::debug { assert(workLine.textLength != 0, "0 width line"); } |
| afterLine = _lines[workIndex+1]; |
| afterLine.setTextLength((newLine.absoluteStart + newLine.textLength) - afterLine.absoluteStart); |
| CONFIG::debug { assert(_lines[workIndex + 1].textLength != 0, "0 width line"); } |
| afterLine.setAbsoluteStart(newLine.absoluteStart + newLine.textLength); |
| _lines.splice(workIndex + 1, 0, newLine); |
| } |
| damageStart = workLine.absoluteStart; |
| } |
| else if (workLine.textLength > newLine.textLength) |
| { |
| // New line partially overlaps old line. |
| // Keep the old line, but resize it so it comes after the new line. |
| // Insert the new line at the old line's position |
| workLine.setTextLength(workLine.textLength - newLine.textLength); |
| CONFIG::debug { assert(workLine.textLength != 0, "0 width line"); } |
| workLine.setAbsoluteStart(newLine.absoluteStart + newLine.textLength); |
| workLine.damage(TextLineValidity.INVALID); |
| _lines.splice(workIndex, 0, newLine); |
| damageStart = workLine.absoluteStart; |
| } |
| else |
| { |
| var deleteCount:int = 1; |
| // The new line completely overlaps the old line. |
| // Insert the new line over the old line. If the line extents don't match, |
| // fix-up the starting position & extent of the following line. |
| if (workLine.textLength != newLine.textLength) |
| { |
| var amtRemaining:int = (newLine.textLength - workLine.textLength); |
| var nextLine:int = workIndex + 1; |
| while (amtRemaining > 0) |
| { |
| afterLine = _lines[nextLine]; |
| if (amtRemaining < afterLine.textLength) |
| { |
| afterLine.setTextLength(afterLine.textLength - amtRemaining); |
| afterLine.damage(TextLineValidity.INVALID); |
| break; |
| } |
| else |
| { |
| deleteCount++; |
| amtRemaining -= afterLine.textLength; |
| nextLine++; |
| afterLine = nextLine < _lines.length ? _lines[nextLine] : null |
| } |
| } |
| if (afterLine && afterLine.absoluteStart != newLine.absoluteStart + newLine.textLength) |
| { |
| afterLine.setAbsoluteStart(newLine.absoluteStart + newLine.textLength); |
| afterLine.damage(TextLineValidity.INVALID); |
| CONFIG::debug { assert(afterLine.textLength != 0, "0 width line"); } |
| } |
| damageStart = newLine.absoluteStart + newLine.textLength; |
| } |
| // remove userData on the deleted lines so they can be recycled |
| if (TextLineRecycler.textLineRecyclerEnabled) |
| { |
| var backgroundManager:BackgroundManager = textFlow.backgroundManager; |
| for (var recycleIdx:int = workIndex; recycleIdx < workIndex+deleteCount; recycleIdx++) |
| { |
| var textLine:TextLine = TextFlowLine(_lines[recycleIdx]).peekTextLine(); |
| if (textLine && !textLine.parent) |
| { |
| // lines shouldn't be valid here but lets check anyhow |
| CONFIG::debug { assert(textLine.validity != TextLineValidity.VALID,"caught a bug here"); } |
| if (textLine.validity != TextLineValidity.VALID) // recycle immediately if not parented |
| { |
| textLine.userData = null; |
| TextLineRecycler.addLineForReuse(textLine); |
| if (backgroundManager) |
| backgroundManager.removeLineFromCache(textLine); |
| } |
| } |
| } |
| } |
| _lines.splice(workIndex, deleteCount, newLine); |
| } |
| |
| if (_damageAbsoluteStart > damageStart) |
| _damageAbsoluteStart = damageStart; |
| // CONFIG::debug { debugCheckTextFlowLines(false); } |
| // CONFIG::debug { checkFirstDamaged(); } enabling this will cause false positives due to _damageAbsoluteStart during composition not updated when GEOMETRY_DAMAGE lines are cleared |
| } |
| |
| /** @private - helper function for finding a base swf context from a swf context */ |
| tlf_internal static function computeBaseSWFContext(context:ISWFContext):ISWFContext |
| { |
| return context && Object(context).hasOwnProperty("getBaseSWFContext") ? context["getBaseSWFContext"]() : context; |
| } |
| /** |
| * The ISWFContext instance used to make FTE calls as needed. |
| * |
| * <p>By default, the ISWFContext implementation is this FlowComposerBase object. |
| * Applications can provide a custom implementation to use fonts |
| * embedded in a different SWF file or to cache and reuse text lines.</p> |
| * |
| * @see flashx.textLayout.compose.ISWFContext |
| * |
| * @playerversion Flash 10 |
| * @playerversion AIR 1.5 |
| * @langversion 3.0 |
| */ |
| |
| public function get swfContext():ISWFContext |
| { |
| return _swfContext; |
| } |
| public function set swfContext(context:ISWFContext):void |
| { |
| if (context != _swfContext) |
| { |
| // swf contexts can be wrappers for other swf contexts - we're going to let the swfcontext give us a hint here |
| if (textFlow) |
| { |
| var newBaseContext:ISWFContext = computeBaseSWFContext(context); |
| var oldBaseContext:ISWFContext = computeBaseSWFContext(_swfContext); |
| |
| _swfContext = context; |
| |
| if (newBaseContext != oldBaseContext) |
| { |
| damage(0,textFlow.textLength,FlowDamageType.INVALID); |
| textFlow.invalidateAllFormats(); |
| } |
| } |
| else |
| _swfContext = context; |
| } |
| } |
| |
| /** |
| * Validate that the lines associated with the flow are internally consistent. |
| * @private |
| * The start of the next line has to match the start of the previous line + the |
| * span of the previous line. The last line needs to end at the end of the flow, |
| * and the first line must be at the start of the text. |
| */ |
| CONFIG::debug public function debugCheckTextFlowLines(validateControllers:Boolean=true):int |
| { |
| var rslt:int = 0; |
| var position:int = 0; |
| var overflow:Boolean = false; |
| for each (var line:TextFlowLine in _lines) |
| { |
| // trace("validateLines:",lines.indexOf(line).toString()," ",line.start," ",line.textLength); |
| rslt += assert(line.absoluteStart == position, "Line start incorrect"); |
| rslt += assert(line.textLength >= 0,"Invalind length"); |
| if (validateControllers) |
| { |
| var lineController:ContainerController = line.controller; |
| if (lineController != null) |
| { |
| rslt += assert(overflow == false,"non overflow line after overflow line?"); |
| rslt += assert(line.absoluteStart >= line.controller.absoluteStart,"bad container mapping"); |
| rslt += assert(line.absoluteStart+line.textLength<= lineController.absoluteStart+lineController.textLength,"bad container mapping"); |
| } |
| else |
| overflow = true; |
| } |
| position += line.textLength; |
| } |
| return rslt; |
| } |
| } |
| } |