//////////////////////////////////////////////////////////////////////////////// // // 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:
*
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.
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
.
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
.
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.
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
.