//////////////////////////////////////////////////////////////////////////////// // // 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.factory { import flash.geom.Rectangle; import flash.text.engine.TextLine; import flashx.textLayout.compose.FlowComposerBase; import flashx.textLayout.container.ScrollPolicy; import flashx.textLayout.debug.assert; import flashx.textLayout.elements.Configuration; import flashx.textLayout.elements.IConfiguration; import flashx.textLayout.elements.ParagraphElement; import flashx.textLayout.elements.SpanElement; import flashx.textLayout.elements.TextFlow; import flashx.textLayout.formats.BlockProgression; import flashx.textLayout.formats.ITextLayoutFormat; import flashx.textLayout.formats.LineBreak; import flashx.textLayout.tlf_internal; use namespace tlf_internal; /** * The StringTextLineFactory class provides a simple way to create TextLines from a string. * *

The text lines are static and are created using a single format and a single paragraph. * The lines are created to fit in the specified bounding rectangle.

* *

The StringTextLineFactory provides an efficient way to create TextLines, since it reuses single TextFlow, * ParagraphElement, SpanElement, and ContainerController objects across many repeated invocations. You can create a * single factory, and use it again and again. You can also reuse all the parts that are the same each time * you call it; for instance, you can reuse the various formats and the bounds.

* *

Note: To create static lines that use multiple formats or paragraphs, or that include * inline graphics, use a TextFlowTextLineFactory and a TextFlow object.

* * @includeExample examples\StringTextLineFactory_example.as -noswf * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 * * @see flashx.textLayout.factory.TextFlowTextLineFactory TextFlowTextLineFactory */ public final class StringTextLineFactory extends TextLineFactoryBase { private var _tf:TextFlow; private var _para:ParagraphElement; private var _span:SpanElement; private var _formatsChanged:Boolean; static private var _defaultConfiguration:Configuration = null; private var _configuration:IConfiguration; /** * The configuration used by the internal TextFlow object. * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function get configuration():IConfiguration { return _configuration; } /** * The default configuration used by this factory if none is specified. * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ static public function get defaultConfiguration():IConfiguration { if (!_defaultConfiguration) { _defaultConfiguration = TextFlow.defaultConfiguration.clone(); _defaultConfiguration.flowComposerClass = getDefaultFlowComposerClass(); _defaultConfiguration.textFlowInitialFormat = null; } return _defaultConfiguration; } /** * Creates a StringTextLineFactory object. * * @param configuration The configuration object used to set the properties of the * internal TextFlow object used to compose lines produced by this factory. * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function StringTextLineFactory(configuration:IConfiguration = null):void { super(); initialize(configuration); } private function initialize(config:IConfiguration):void { _configuration = Configuration(config ? config : defaultConfiguration).getImmutableClone(); _tf = new TextFlow(_configuration); _para = new ParagraphElement(); _span = new SpanElement(); _para.replaceChildren(0, 0, _span); _tf.replaceChildren(0, 0, _para); _tf.flowComposer.addController(containerController); _formatsChanged = true; } /** * The character format. * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function get spanFormat():ITextLayoutFormat { return _span.format; } public function set spanFormat(format:ITextLayoutFormat):void { _span.format = format; _formatsChanged = true; } /** * The paragraph format. * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function get paragraphFormat():ITextLayoutFormat { return _para.format; } public function set paragraphFormat(format:ITextLayoutFormat):void { _para.format = format; _formatsChanged = true; } /** * The text flow format. * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function get textFlowFormat():ITextLayoutFormat { return _tf.format; } public function set textFlowFormat(format:ITextLayoutFormat):void { _tf.format = format; _formatsChanged = true; } /** * The text to convert into TextLine objects. * *

To produce TextLines, call createTextLines() after setting this * text property and the desired formats.

* @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function get text():String { return _span.text; } public function set text(string:String):void { _span.text = string; } /** @private Used for measuring the truncation indicator */ static private var _measurementFactory:StringTextLineFactory = null; static private function measurementFactory():StringTextLineFactory { if (_measurementFactory == null) _measurementFactory = new StringTextLineFactory(); return _measurementFactory; } static private var _measurementLines:Array = null; static private function measurementLines():Array { if (_measurementLines == null) _measurementLines = new Array(); return _measurementLines; } /** * Creates TextLine objects using the text currently assigned to this factory object. * *

The text lines are created using the currently assigned text and formats and * are composed to fit the bounds assigned to the compositionBounds property. * As each line is created, the factory calls the function specified in the * callback parameter. This function is passed the TextLine object and * is responsible for displaying the line.

* *

To create a different set of lines, change any properties desired and call * createTextLines() again.

* * @param callback The callback function called for each TextLine object created. * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function createTextLines(callback:Function):void { createTextLinesInternal(callback); _factoryComposer._lines.splice(0); if (_pass0Lines) _pass0Lines.splice(0); } /** Internal version preserves generated lines */ private function createTextLinesInternal(callback:Function):void { var measureWidth:Boolean = !compositionBounds || isNaN(compositionBounds.width); var measureHeight:Boolean = !compositionBounds || isNaN(compositionBounds.height); CONFIG::debug { assert(_tf.flowComposer.numControllers == 1,"DisplayController bad number containers"); } CONFIG::debug { assert(containerController == _tf.flowComposer.getControllerAt(0),"ContainerController mixup"); } var bp:String = _tf.computedFormat.blockProgression; containerController.setCompositionSize(compositionBounds.width, compositionBounds.height); // override scroll policy if truncation options are set containerController.verticalScrollPolicy = truncationOptions ? ScrollPolicy.OFF : verticalScrollPolicy; containerController.horizontalScrollPolicy = truncationOptions ? ScrollPolicy.OFF : horizontalScrollPolicy; _isTruncated = false; _truncatedText = text; if (!_formatsChanged && FlowComposerBase.computeBaseSWFContext(_tf.flowComposer.swfContext) != FlowComposerBase.computeBaseSWFContext(swfContext)) _formatsChanged = true; _tf.flowComposer.swfContext = swfContext; if (_formatsChanged) { _tf.normalize(); _formatsChanged = false; } _tf.flowComposer.compose(); // Need truncation if all the following are true // - truncation options exist // - content doesn't fit if (truncationOptions && !doesComposedTextFit(truncationOptions.lineCountLimit, _tf.textLength, bp)) { _isTruncated = true; var somethingFit:Boolean = false; // were we able to fit something? var originalText:String = _span.text; computeLastAllowedLineIndex (truncationOptions.lineCountLimit); if (_truncationLineIndex >= 0) { // Estimate the initial truncation position using the following steps // 1. Measure the space that the truncation indicator will take measureTruncationIndicator (compositionBounds, truncationOptions.truncationIndicator); // 2. Move target line for truncation higher by as many lines as the number of full lines taken by the truncation indicator _truncationLineIndex -= (_measurementLines.length -1); if (_truncationLineIndex >= 0) { var truncateAtCharPosition:int; if (_tf.computedFormat.lineBreak == LineBreak.EXPLICIT || (bp == BlockProgression.TB ? measureWidth : measureHeight)) { // 3., 4. Initial truncation position: end of the last allowed line var line:TextLine = _factoryComposer._lines[_truncationLineIndex] as TextLine; truncateAtCharPosition = line.userData + line.rawTextLength; } else { // 3. Calculate allowed width (width left over from the last line of the truncation indicator) var targetWidth:Number = (bp == BlockProgression.TB ? compositionBounds.width : compositionBounds.height); if (paragraphFormat) { targetWidth -= (Number(paragraphFormat.paragraphSpaceAfter) + Number(paragraphFormat.paragraphSpaceBefore)); if (_truncationLineIndex == 0) targetWidth -= paragraphFormat.textIndent; } var allowedWidth:Number = targetWidth - (_measurementLines[_measurementLines.length-1] as TextLine).unjustifiedTextWidth; // 4. Get the initial truncation position on the target line given this allowed width truncateAtCharPosition = getTruncationPosition(_factoryComposer._lines[_truncationLineIndex], allowedWidth); } // Save off the original lines: used in getNextTruncationPosition if (!_pass0Lines) _pass0Lines = new Array(); _pass0Lines = _factoryComposer.swapLines(_pass0Lines); // Note that for the original lines to be valid when used, the containing text block should not be modified // Cloning the paragraph ensures this _para = _para.deepCopy() as ParagraphElement; _span = _para.getChildAt(0) as SpanElement; _tf.replaceChildren(0, 1, _para); _tf.normalize(); // Replace all content starting at the inital truncation position with the truncation indicator _span.replaceText(truncateAtCharPosition, _span.textLength, truncationOptions.truncationIndicator); // The following loop executes repeatedly composing text until it fits // In each iteration, an atoms's worth of characters of original content is dropped do { _tf.flowComposer.compose(); if (doesComposedTextFit(truncationOptions.lineCountLimit, _tf.textLength, bp)) { somethingFit = true; break; } if (truncateAtCharPosition == 0) break; // no original content left to make room for truncation indicator // Try again by truncating at the beginning of the preceding atom var newTruncateAtCharPosition:int = getNextTruncationPosition(truncateAtCharPosition); _span.replaceText(newTruncateAtCharPosition, truncateAtCharPosition, null); truncateAtCharPosition = newTruncateAtCharPosition; } while (true); } _measurementLines.splice(0); // cleanup } if (somethingFit) _truncatedText = _span.text; else { _truncatedText = ""; _factoryComposer._lines.splice(0); // return no lines } _span.text = originalText; } var xadjust:Number = compositionBounds.x; // toptobottom sets zero to the right edge - adjust the locations var controllerBounds:Rectangle = containerController.getContentBounds(); if (bp == BlockProgression.RL) xadjust += (measureWidth ? controllerBounds.width : compositionBounds.width); // apply x and y adjustment to the bounds controllerBounds.left += xadjust; controllerBounds.right += xadjust; controllerBounds.top += compositionBounds.y; controllerBounds.bottom += compositionBounds.y; callbackWithTextLines(callback,xadjust,compositionBounds.y); setContentBounds(controllerBounds); } /** @private * Gets the text that is composed in the preceding call to createTextLines * If no truncation is performed, a string equal to text is returned. * If truncation is performed, but nothing fits, an empty string is returned. * Otherwise, a substring of text followed by the truncation indicator is returned. */ tlf_internal function get truncatedText():String { return _truncatedText; } private var _truncatedText:String; /** * Measures the truncation indicator using the same bounds and formats, but without truncation options * Resultant lines are added to _measurementLines */ private function measureTruncationIndicator (compositionBounds:Rectangle, truncationIndicator:String):void { var originalLines:Array = _factoryComposer.swapLines(measurementLines()); // ensure we don't overwrite original lines var measureFactory:StringTextLineFactory = measurementFactory(); measureFactory.compositionBounds = compositionBounds; measureFactory.text = truncationIndicator; measureFactory.spanFormat = spanFormat; measureFactory.paragraphFormat = paragraphFormat; measureFactory.textFlowFormat = textFlowFormat; measureFactory.truncationOptions = null; measureFactory.createTextLinesInternal(noOpCallback); _factoryComposer.swapLines(originalLines); // restore } /** * Gets the truncation position on a line given the allowed width * - Must be at an atom boundary * - Must scan the line for atoms in logical order, not physical position order * For example, given bi-di text ABאבCD * atoms must be scanned in this order * A, B, א * ג, C, D */ private function getTruncationPosition (line:TextLine, allowedWidth:Number):uint { var consumedWidth:Number = 0; var charPosition:int = line.userData; // start of line while (charPosition < line.userData + line.rawTextLength) // end of line { var atomIndex:int = line.getAtomIndexAtCharIndex(charPosition); var atomBounds:Rectangle = line.getAtomBounds(atomIndex); consumedWidth += atomBounds.width; if (consumedWidth > allowedWidth) break; charPosition = line.getAtomTextBlockEndIndex(atomIndex); } line.flushAtomData(); return charPosition; } private function noOpCallback(line:TextLine):void { } } }