| //////////////////////////////////////////////////////////////////////////////// |
| // |
| // 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.display.Shape; |
| import flash.geom.Rectangle; |
| import flash.text.engine.TextLine; |
| |
| import flashx.textLayout.compose.FlowComposerBase; |
| import flashx.textLayout.compose.SimpleCompose; |
| 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. |
| * |
| * <p>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.</p> |
| * |
| * <p>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.</p> |
| * |
| * <p><b>Note:</b> To create static lines that use multiple formats or paragraphs, or that include |
| * inline graphics, use a TextFlowTextLineFactory and a TextFlow object. </p> |
| * |
| * <p><b>Note:</b> The StringTextLineFactory ignores the truncationIndicatorFormat property set in the truncationOptions when truncating text.</p> |
| * |
| * @includeExample examples\StringTextLineFactory_example.as -noswf |
| * |
| * @playerversion Flash 10 |
| * @playerversion AIR 1.5 |
| * @langversion 3.0 |
| * |
| * @see flashx.textLayout.factory.TextFlowTextLineFactory TextFlowTextLineFactory |
| */ |
| public 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) |
| { |
| 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. |
| * |
| * <p>To produce TextLines, call <code>createTextLines()</code> after setting this |
| * <code>text</code> property and the desired formats.</p> |
| * @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. |
| * |
| * <p>The text lines are created using the currently assigned text and formats and |
| * are composed to fit the bounds assigned to the <code>compositionBounds</code> property. |
| * As each line is created, the factory calls the function specified in the |
| * <code>callback</code> parameter. This function is passed the TextLine object and |
| * is responsible for displaying the line.</p> |
| * |
| * <p>To create a different set of lines, change any properties desired and call |
| * <code>createTextLines()</code> again.</p> |
| * |
| * <p>Note that the scroll policies of the factory will control how many lines are generated.</p> |
| * |
| * @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 |
| { |
| var saved:SimpleCompose = TextLineFactoryBase.beginFactoryCompose(); |
| try |
| { |
| createTextLinesInternal(callback); |
| } |
| finally |
| { |
| _factoryComposer._lines.splice(0); |
| if (_pass0Lines) |
| _pass0Lines.splice(0); |
| TextLineFactoryBase.endFactoryCompose(saved); |
| } |
| } |
| |
| /** 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) |
| doTruncation(bp, measureWidth, measureHeight); |
| |
| 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; |
| |
| if (_tf.backgroundManager) |
| processBackgroundColors(_tf,callback,xadjust,compositionBounds.y,containerController.compositionWidth,containerController.compositionHeight); |
| callbackWithTextLines(callback,xadjust,compositionBounds.y); |
| |
| setContentBounds(controllerBounds); |
| containerController.clearCompositionResults(); |
| } |
| |
| // Need truncation if all the following are true |
| // - truncation options exist |
| // - content doesn't fit |
| /** @private */ |
| tlf_internal function doTruncation(bp:String, measureWidth:Boolean, measureHeight:Boolean):void |
| { |
| var bp:String = _tf.computedFormat.blockProgression; |
| if (!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; |
| } |
| } |
| |
| /** @private |
| * Gets the text that is composed in the preceding call to <code>createTextLines</code> |
| * If no truncation is performed, a string equal to <code>text</code> is returned. |
| * If truncation is performed, but nothing fits, an empty string is returned. |
| * Otherwise, a substring of <code>text</code> 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(noopfunction); |
| _factoryComposer.swapLines(originalLines); // restore |
| } |
| |
| static private function noopfunction(o:Object):void // No PMD |
| { } |
| |
| |
| /** |
| * 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; |
| } |
| |
| } |
| } |