//////////////////////////////////////////////////////////////////////////////// // // 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.display.Sprite; import flash.system.Capabilities; import flash.text.engine.TextLine; import flashx.textLayout.accessibility.TextAccImpl; import flashx.textLayout.container.ContainerController; import flashx.textLayout.container.ScrollPolicy; import flashx.textLayout.debug.assert; import flashx.textLayout.edit.ISelectionManager; import flashx.textLayout.elements.BackgroundManager; import flashx.textLayout.elements.ContainerFormattedElement; import flashx.textLayout.elements.ParagraphElement; import flashx.textLayout.elements.TextFlow; import flashx.textLayout.events.CompositionCompleteEvent; import flashx.textLayout.formats.BlockProgression; import flashx.textLayout.tlf_internal; use namespace tlf_internal; [Exclude(name="createBackgroundManager",kind="method")] /** * The StandardFlowComposer class provides a standard composer and container manager. * *

Each call to compose() or updateAllControllers() normalizes the text flow as a first step. * The normalizing process checks the parts of the TextFlow object that were modified and takes the following steps: *

    *
  1. Deletes empty FlowLeafElement and SubParagraphGroupElement objects.
  2. *
  3. Merges sibling spans that have identical attributes.
  4. *
  5. Adds an empty paragraph if a flow is empty.
  6. *
*

* *

To use a StandardFlowComposer, assign it to the * flowComposer property of a TextFlow object. Call the updateAllControllers() * method to lay out and display the text in the containers attached to the flow composer.

* *

Note: For simple, static text flows, you can also use one of the text line factory classes. * These factory classes will typically create lines with less overhead than a flow composer, but do not * support editing, dynamic changes, or user interaction.

* * @see flashx.textLayout.elements.TextFlow#flowComposer * @includeExample examples\StandardFlowComposer_ClassExample.as -noswf * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public class StandardFlowComposer extends FlowComposerBase implements IFlowComposer { /** @private */ tlf_internal var _rootElement:ContainerFormattedElement; private var _controllerList:Array; private var _composing:Boolean; /** * Creates a StandardFlowComposer object. * *

To use an StandardFlowComposer object, assign it to the * flowComposer property of a TextFlow object. Call the updateAllControllers() * method to lay out and display the text in the containers attached to the flow composer.

* * @includeExample examples\StandardFlowComposer_constructor.as -noswf * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function StandardFlowComposer():void { super(); _controllerList = new Array(); _composing = false; } /** * True, if the flow composer is currently performing a composition operation. * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function get composing():Boolean { return _composing; } /** * Returns the absolute position of the first content element in the specified ContainerController object. * *

A position is calculated by counting the division between two characters or other elements of a text flow. * The position preceding the first element of a flow is zero. An absolute position is the position * counting from the beginning of the flow.

* * @param controller A ContainerController object associated with this flow composer. * @return the position before the first character or graphic in the ContainerController. * * @includeExample examples\StandardFlowComposer_getAbsoluteStart.as -noswf * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function getAbsoluteStart(controller:ContainerController):int { // don't look at controller's relativeStart property - it uses this method. hmmmm // TODO: that does seem odd - clean the above implementation up. var stopIdx:int = getControllerIndex(controller); CONFIG::debug { assert(stopIdx != -1,"bad controller to LayoutFlowComposer.getRelativeStart"); } var rslt:int = _rootElement.getAbsoluteStart(); for (var idx:int = 0; idx < stopIdx; idx++) rslt += _controllerList[idx].textLength; return rslt; } /** @copy IFlowComposer#rootElement * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function get rootElement():ContainerFormattedElement { return _rootElement; } /** @copy IFlowComposer#setRootElement() * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function setRootElement(newRootElement:ContainerFormattedElement):void { if (_rootElement != newRootElement) { if (newRootElement is TextFlow && TextFlow(newRootElement).flowComposer != this) TextFlow(newRootElement).flowComposer = this; else { clearCompositionResults(); detachAllContainers(); _rootElement = newRootElement; _textFlow = _rootElement ? _rootElement.getTextFlow() : null; attachAllContainers(); } } } /** @private */ tlf_internal function detachAllContainers():void { // detatch accessibility from the containers // Why only the first container? if (_controllerList.length > 0 && _textFlow) { var firstContainerController:ContainerController = getControllerAt(0); var firstContainer:Sprite = firstContainerController.container; if (firstContainer) clearContainerAccessibilityImplementation(firstContainer); } var cont:ContainerController; for each (cont in _controllerList) { cont.clearSelectionShapes(); cont.setRootElement(null); } } static private function clearContainerAccessibilityImplementation(cont:Sprite):void { if (cont.accessibilityImplementation) { if (cont.accessibilityImplementation is TextAccImpl) TextAccImpl(cont.accessibilityImplementation).detachListeners(); cont.accessibilityImplementation = null; } } /** @private */ tlf_internal function attachAllContainers():void { var cont:ContainerController; for each (cont in _controllerList) ContainerController(cont).setRootElement(_rootElement); if (_controllerList.length > 0 && _textFlow) { // attach accessibility to the containers // Why only the first container? There are workflows that this will fail // for example: a pagination workflow that has a composed chain of containers but only displays one at a time. if (textFlow.configuration.enableAccessibility && flash.system.Capabilities.hasAccessibility) { var firstContainer:Sprite = getControllerAt(0).container; if (firstContainer) { clearContainerAccessibilityImplementation(firstContainer); firstContainer.accessibilityImplementation = new TextAccImpl(firstContainer, _textFlow); } } var curContainer:Sprite; // turn off focusRect on all containers for (var i:int = 0; i < _controllerList.length; ++i) { curContainer = getControllerAt(i).container; if (curContainer) curContainer.focusRect = false; } } // TODO: can be more efficient? - just damage all clearCompositionResults(); } /** @copy IFlowComposer#numControllers * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function get numControllers():int { return _controllerList ? _controllerList.length : 0; } /** @copy IFlowComposer#addController() * * @includeExample examples\StandardFlowComposer_addController.as -noswf * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function addController(controller:ContainerController):void { CONFIG::debug { assert (_controllerList.indexOf(controller) < 0, "adding controller twice"); } _controllerList.push(ContainerController(controller)); if (this.numControllers == 1) { attachAllContainers(); } else { controller.setRootElement(_rootElement) var curContainer:Sprite = controller.container; if (curContainer) curContainer.focusRect = false; if (textFlow) { // mark the previous container as geometry damaged - this is more than is needed controller = this.getControllerAt(this.numControllers-2); var damageStart:int = controller.absoluteStart; var damageLen:int = controller.textLength; // watch out for an empty previous container if (damageLen == 0) { if (damageStart != textFlow.textLength) damageLen++; else if (damageStart != 0) { damageStart--; damageLen++; } } if (damageLen) textFlow.damage(damageStart,damageLen,FlowDamageType.GEOMETRY,false); } } } /** @copy IFlowComposer#addControllerAt() * * @includeExample examples\StandardFlowComposer_addControllerAt.as -noswf * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function addControllerAt(controller:ContainerController, index:int):void { CONFIG::debug { assert (_controllerList.indexOf(controller) == -1, "adding controller twice"); } detachAllContainers(); _controllerList.splice(index,0,ContainerController(controller)); attachAllContainers(); } /** Removes a trailing controller with no content without doing any damage */ private function fastRemoveController(index:int):Boolean { if (index == -1) return true; var cont:ContainerController = _controllerList[index]; if (!cont) return true; if (!_textFlow || cont.absoluteStart == _textFlow.textLength) { if (index == 0) { var firstContainer:Sprite = cont.container; if (firstContainer) clearContainerAccessibilityImplementation(firstContainer); } cont.setRootElement(null); _controllerList.splice(index,1); return true; } return false; } /** @copy IFlowComposer#removeController() * * @includeExample examples\StandardFlowController_removeController.as -noswf * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function removeController(controller:ContainerController):void { var index:int = getControllerIndex(controller); if (!fastRemoveController(index)) { detachAllContainers(); _controllerList.splice(index,1); attachAllContainers(); } } /** @copy IFlowComposer#removeControllerAt() * * @includeExample examples\StandardFlowController_removeControllerAt.as -noswf * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function removeControllerAt(index:int):void { if (!fastRemoveController(index)) { detachAllContainers(); _controllerList.splice(index,1); attachAllContainers(); } } /** @copy IFlowComposer#removeAllControllers() * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function removeAllControllers():void { detachAllContainers(); _controllerList.splice(0,_controllerList.length); } /** @copy IFlowComposer#getControllerAt() * * @includeExample examples\StandardFlowComposer_getControllerAt.as -noswf * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function getControllerAt(index:int):ContainerController { return _controllerList[index]; } /** @copy IFlowComposer#getControllerIndex() * * @includeExample examples\StandardFlowComposer_getControllerIndex.as -noswf * @playerversion Flash 10 * @player version AIR 1.5 * @langversion 3.0 * @playerversion AIR 1.5 */ public function getControllerIndex(controller:ContainerController):int { // TODO: binary search? for (var idx:int = 0; idx < _controllerList.length; idx++) { if (_controllerList[idx] == controller) return idx; } return -1; } /** * Returns the index of the controller containing the content at the specified position. * *

A position can be considered to be the division between two characters or other elements of a text flow. If * the value in absolutePosition is a position between the last character of one * container and the first character of the next, then the preceding container is returned if * the preferPrevious parameter is set to true and the later container is returned if * the preferPrevious parameter is set to false.

* *

The method returns -1 if the content at the specified position is not in any container or is outside * the range of positions in the text flow.

* * @param absolutePosition The position of the content for which the container index is sought. * @param preferPrevious Specifies which container index to return when the position is between the last element in * one container and the first element in the next. * * @return the index of the container controller or -1 if not found. * * @includeExample examples\StandardFlowComposer_findControllerIndexAtPosition.as -noswf * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function findControllerIndexAtPosition(absolutePosition:int,preferPrevious:Boolean=false):int { var lo:int = 0; var hi:int = _controllerList.length-1; while (lo <= hi) { var mid:int = (lo+hi)/2; var cont:ContainerController = _controllerList[mid]; if (cont.absoluteStart <= absolutePosition) { if (preferPrevious) { if (cont.absoluteStart + cont.textLength >= absolutePosition) { // find first container or first one with non-zero textLength while (mid != 0 && cont.absoluteStart == absolutePosition) { mid--; cont = _controllerList[mid]; } return mid; } } else { if (cont.absoluteStart == absolutePosition && cont.textLength != 0) { while (mid != 0) { cont = _controllerList[mid-1]; if (cont.textLength != 0) break; mid--; } return mid; } if (cont.absoluteStart + cont.textLength > absolutePosition) return mid; } lo = mid+1; } else hi = mid-1; } return -1; } /** Clear whatever computed values are left from the last composition, in the flow composer and * in each of its controllers. @private */ tlf_internal function clearCompositionResults():void { initializeLines(); for each (var cont:ContainerController in _controllerList) cont.clearCompositionResults(); } /** * Composes the content of the root element and updates the display. * *

Text layout is conducted in two phases: composition and display. In the composition phase, * the flow composer calculates how many lines are necessary to display the content as well as the position of these * lines in the flow's display containers. In the display phase, * the flow composer updates the display object children of its containers. The updateAllControllers() * method initiates both phases in sequence. The StandardFlowComposer keeps track of changes to content * so that a full cycle of composition and display is only performed when necessary.

* *

This method updates all the text lines and the display list immediately and synchronously.

* *

If the contents of any container is changed, the method returns true.

* * @return true if anything changed. * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 * */ public function updateAllControllers():Boolean { return updateToController(); } /** * Composes and updates the display up to and including the container at the specified index. * *

The updateToController() method composes the content and * updates the display of all containers up to and including the container at the specified index. * For example, if you have a chain of 20 containers and specify an index of 10, * updateToController() ensures that the first through the tenth (indexes 0-9) * containers are composed and displayed. Composition stops at that point. If controllerIndex * is -1 (or not specified), then all containers are updated.

* *

This method updates all the text lines and the display list immediately and synchronously.

* *

If the contents of any container is changed, the method returns true.

* * @param controllerIndex index of the last container to update (by default updates all containers) * @return true, if anything changed. * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 * */ public function updateToController(index:int = int.MAX_VALUE):Boolean { //CONFIG::debug { assert(!_composing,"updateToController: compose in process"); } if (_composing) return false; //note that this will always update the display AND update the //selection. So, even if nothing has changed that would cause //a recompose, the selection would still be redrawn. var sm:ISelectionManager = textFlow.interactionManager; if (sm) sm.flushPendingOperations(); var startController:ContainerController = _composing ? null : internalCompose(-1, index); var shapesDamaged:Boolean = areShapesDamaged(); if (shapesDamaged) updateCompositionShapes(); if (sm) sm.refreshSelection(); releaseLines(startController); return shapesDamaged; } /** * Sets the focus to the container that contains the location specified by the absolutePosition * parameter. * *

The StandardFlowComposer calls the setFocus() method of the ContainerController object * containing the specified text flow position.

* * @param absolutePosition Specifies the position in the text flow of the container to receive focus. * @param preferPrevious If true and the position is before the first character in a container, sets focus to the end of * the previous container. * * @see flash.display.Stage#focus * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function setFocus(absolutePosition:int,leanLeft:Boolean=false):void { var idx:int = findControllerIndexAtPosition(absolutePosition,leanLeft); if (idx == -1) idx = this.numControllers-1; if (idx != -1) _controllerList[idx].setFocus(); } /** * Called by the TextFlow when the interaction manager changes. * *

This function is called automatically. Your code does not typically need to call this * method. Classes that extend StandardFlowComposer can override this method to update * event listeners and other properties that depend on the interaction manager.

* * @param newInteractionManager The new ISelectionManager instance. * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function interactionManagerChanged(newInteractionManager:ISelectionManager):void { for each (var controller:ContainerController in _controllerList) controller.interactionManagerChanged(newInteractionManager); } private function updateCompositionShapes():void { for each (var controller:ContainerController in _controllerList) controller.updateCompositionShapes(); } //-------------------------------------------------------------------------- // // Composition // //-------------------------------------------------------------------------- /** @private Override required because we may be damaged if the last container has scrolling */ public override function isDamaged(absolutePosition:int):Boolean { // Returns true if any text from _damageAbsoluteStart through absolutePosition needs to be recomposed if (!super.isDamaged(absolutePosition)) { if (absolutePosition == _textFlow.textLength) { var container:ContainerController = getControllerAt(numControllers-1); if (container && (container.verticalScrollPolicy != ScrollPolicy.OFF || container.horizontalScrollPolicy != ScrollPolicy.OFF)) return true; } return false; } return true; } /** Returns true if composition is necessary, false otherwise */ protected function preCompose():Boolean { CONFIG::debug { checkFirstDamaged(); } rootElement.preCompose(); // No content, nothing to compose - TextFlow isn't loaded or connected CONFIG::debug { assert(rootElement.textLength != 0,"bad TextFlow after normalize"); } // brand new content if (numLines == 0) initializeLines(); return isDamaged(rootElement.getAbsoluteStart() + rootElement.textLength); } /** @private */ tlf_internal function getComposeState():ComposeState { return ComposeState.getComposeState(); } /** @private */ tlf_internal function releaseComposeState(state:ComposeState):void { ComposeState.releaseComposeState(state); } /** @private Return the first damaged controller */ tlf_internal function callTheComposer(composeToPosition:int, controllerEndIndex:int):ContainerController { if (_damageAbsoluteStart == rootElement.getAbsoluteStart()+rootElement.textLength) return getControllerAt(numControllers-1);; var state:ComposeState = getComposeState(); var lastComposedPosition:int = state.composeTextFlow(textFlow, composeToPosition, controllerEndIndex); if (_damageAbsoluteStart < lastComposedPosition) _damageAbsoluteStart = lastComposedPosition; CONFIG::debug { checkFirstDamaged(); } // make sure there is an empty TextFlowLine covering any trailing content finalizeLinesAfterCompose(); var startController:ContainerController = state.startController; releaseComposeState(state); textFlow.dispatchEvent(new CompositionCompleteEvent(CompositionCompleteEvent.COMPOSITION_COMPLETE,false,false,textFlow, 0,lastComposedPosition)); CONFIG::debug { textFlow.debugCheckTextFlow(); } return startController; } private var lastBPDirectionScrollPosition:Number = Number.NEGATIVE_INFINITY; static private function getBPDirectionScrollPosition(bp:String,cont:ContainerController):Number { return bp == BlockProgression.TB ? cont.verticalScrollPosition : cont.horizontalScrollPosition; } /** Bottleneck function for all types of compose. Does the work of compose, no matter how it is called. @private * @return first controller with changed shapes */ private function internalCompose(composeToPosition:int = -1, composeToControllerIndex:int = -1):ContainerController { var sm:ISelectionManager = textFlow.interactionManager; if (sm) sm.flushPendingOperations(); if (numControllers == 0) return null; if (composeToControllerIndex < 0) { if (composeToPosition >= 0 && damageAbsoluteStart >= composeToPosition) return null; } else { var controller:ContainerController = getControllerAt(Math.min(composeToControllerIndex,numControllers-1)); if (damageAbsoluteStart > controller.absoluteStart+controller.textLength) return null; } // trace("internalCompose: damageAbsoluteStart",damageAbsoluteStart); var lastController:ContainerController; var bp:String; if (composeToControllerIndex == numControllers-1) { lastController = this.getControllerAt(numControllers-1); // skip it if damageAbsoluteStart is past the end of the controller. are there risks here? AND scrollpositions haven't changed since last composeToControllerIndex var a:Array = lastController.findFirstAndLastVisibleLine(); var lastVisibleLine:TextFlowLine = a[1]; if (lastVisibleLine) { bp = rootElement.computedFormat.blockProgression if (getBPDirectionScrollPosition(bp,lastController) == this.lastBPDirectionScrollPosition && damageAbsoluteStart >= lastVisibleLine.absoluteStart+lastVisibleLine.textLength) return null; } } lastBPDirectionScrollPosition = Number.NEGATIVE_INFINITY; CONFIG::debug { assert(_composing == false,"internalCompose: Recursive call"); } _composing = true; var startController:ContainerController; try { var cont:ContainerController; // scratch if (textFlow && numControllers != 0) { if (preCompose()) { startController = callTheComposer(composeToPosition, composeToControllerIndex); if (startController) { var idx:int = this.getControllerIndex(startController); while (idx < numControllers) getControllerAt(idx++).shapesInvalid = true; } } } } catch (e:Error) { _composing = false; throw(e); } _composing = false; if (lastController) { lastBPDirectionScrollPosition = getBPDirectionScrollPosition(bp,lastController); } return startController; } /** @private */ tlf_internal function areShapesDamaged():Boolean { var cont:ContainerController; // scratch // TODO: a flag on this? for each (cont in _controllerList) { if (cont.shapesInvalid) return true; } return false; } /** * Calculates how many lines are necessary to display the content in the root element of the flow and the positions of these * lines in the flow's display containers. * *

The compose() method only composes content if it has changed since the last composition operation. * Results are saved so that subsequent * calls to compose() or updateAllControllers() do not perform an additional recomposition * if the flow content has not changed.

* *

If the contents of any container have changed, the method returns true.

* * @return true if anything changed. * * @includeExample examples\StandardFlowComposer_compose.as -noswf * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 * * @see #updateAllControllers() * @see #updateToController() */ public function compose():Boolean { //CONFIG::debug { assert(!_composing,"compose: compose in process"); } return _composing ? false : internalCompose() != null; } /** @copy IFlowComposer#composeToPosition() * * @includeExample examples\StandardFlowComposer_composeToPosition.as -noswf * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function composeToPosition(absolutePosition:int = int.MAX_VALUE):Boolean { //CONFIG::debug { assert(!_composing,"composeToPosition: compose in process"); } return _composing ? false : internalCompose(absolutePosition, -1) != null; } /** @copy IFlowComposer#composeToController() * * @includeExample examples\StandardFlowComposer_composeToController.as -noswf * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function composeToController(index:int = int.MAX_VALUE):Boolean { //CONFIG::debug { assert(!_composing,"composeToController: compose in process"); } return _composing ? false : internalCompose(-1, index) != null; } /** Release lines in paragraphs that aren't referenced externally, so that they can be garbage collected * if necessary. Iterates through the lines, looking for lines that do not have a valid parent. If all the * lines in a paragraph have no parent, we call the paragraph's TextBlock.releaseLines(). */ private function releaseLines(startController:ContainerController):void { var currentParagraph:ParagraphElement = null; var inUse:Boolean = false; var lastLine:int = lines.length; for (var lineIndex:int = startController ? findLineIndexAtPosition(startController.absoluteStart) : 0; lineIndex < lastLine; lineIndex++) { var line:TextFlowLine = lines[lineIndex]; var paragraph:ParagraphElement = line.paragraph; if (paragraph != currentParagraph) { // We're on a new paragraph. Release the lines from the old para, if they weren't used, // and set up the new para. if (!inUse && currentParagraph) currentParagraph.releaseTextBlock(); currentParagraph = paragraph; inUse = false; } if (!inUse && !line.isDamaged()) { var textLine:TextLine = line.getTextLine(); if (textLine != null && textLine.parent != null) inUse = true; } } // Release the lines from the last para, if they weren't used. if (!inUse && currentParagraph && currentParagraph.hasBlockElement()) currentParagraph.releaseTextBlock(); } /** @private */ public function createBackgroundManager():BackgroundManager { return new BackgroundManager(); } } }