//////////////////////////////////////////////////////////////////////////////// // // 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.elements.BackgroundManager; import flashx.textLayout.debug.Debugging; import flashx.textLayout.debug.assert; 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. * *

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).

* * @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():void { _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 { // 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. // We do this only if we're not on the first line of the paragraph -- figuring this out is expensive but otherwise we could damage // back while we're composing because we damaged in the process of constructing a textBlock. var lineIndex:int = findLineIndexAtPosition(startPosition); var leaf:FlowLeafElement = textFlow.findLeaf(startPosition); if (leaf && lineIndex > 0 && leaf.getParagraph().getAbsoluteStart() != startPosition) 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. * *

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.

* * @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; } } }