blob: 355e6c59a0a7ae8a7c9689af7674b8ec94ee6528 [file] [log] [blame]
////////////////////////////////////////////////////////////////////////////////
//
// 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 bpString:String = _tf.computedFormat.blockProgression;
if (!doesComposedTextFit(truncationOptions.lineCountLimit, _tf.textLength, bpString))
{
_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 || (bpString == 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 = (bpString == 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, bpString))
{
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;
}
}
}