| //////////////////////////////////////////////////////////////////////////////// |
| // |
| // 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.DisplayObject; |
| import flash.display.GraphicsPathCommand; |
| import flash.display.GraphicsPathWinding; |
| import flash.display.Shape; |
| import flash.geom.Point; |
| import flash.geom.Rectangle; |
| import flash.text.engine.TextBlock; |
| import flash.text.engine.TextLine; |
| import flash.text.engine.TextLineValidity; |
| import flash.text.engine.TextRotation; |
| import flash.utils.Dictionary; |
| |
| import flashx.textLayout.container.ContainerController; |
| import flashx.textLayout.debug.Debugging; |
| import flashx.textLayout.debug.assert; |
| import flashx.textLayout.edit.ISelectionManager; |
| import flashx.textLayout.edit.SelectionFormat; |
| import flashx.textLayout.elements.ContainerFormattedElement; |
| import flashx.textLayout.elements.FlowElement; |
| import flashx.textLayout.elements.FlowLeafElement; |
| import flashx.textLayout.elements.FlowValueHolder; |
| import flashx.textLayout.elements.InlineGraphicElement; |
| import flashx.textLayout.elements.ParagraphElement; |
| import flashx.textLayout.elements.SpanElement; |
| import flashx.textLayout.elements.SubParagraphGroupElement; |
| import flashx.textLayout.elements.TCYElement; |
| import flashx.textLayout.elements.TextFlow; |
| import flashx.textLayout.external.WeakRef; |
| import flashx.textLayout.formats.BackgroundColor; |
| import flashx.textLayout.formats.BlockProgression; |
| import flashx.textLayout.formats.Direction; |
| import flashx.textLayout.formats.Float; |
| import flashx.textLayout.formats.ITextLayoutFormat; |
| import flashx.textLayout.formats.JustificationRule; |
| import flashx.textLayout.formats.LeadingModel; |
| import flashx.textLayout.formats.LineBreak; |
| import flashx.textLayout.formats.TextDecoration; |
| import flashx.textLayout.formats.TextLayoutFormat; |
| import flashx.textLayout.tlf_internal; |
| import flashx.textLayout.utils.CharacterUtil; |
| |
| use namespace tlf_internal; |
| |
| |
| /** |
| * The TextFlowLine class represents a single line of text in a text flow. |
| * |
| * <p>Use this class to access information about how a line of text has been composed: its position, |
| * height, width, and so on. When the text flow (TextFlow) is modified, the lines immediately before and at the |
| * site of the modification are marked as invalid because they need to be recomposed. Lines after |
| * the site of the modification might not be damaged immediately, but they might be regenerated once the |
| * text is composed. You can access a TextFlowLine that has been damaged, but any values you access |
| * reflect the old state of the TextFlow. When the TextFlow is recomposed, it generates new lines and you can |
| * get the new line for a given position by calling <code>TextFlow.flowComposer.findLineAtPosition()</code>.</p> |
| * |
| * @playerversion Flash 10 |
| * @playerversion AIR 1.5 |
| * @langversion 3.0 |
| */ |
| |
| public final class TextFlowLine implements IVerticalJustificationLine |
| { |
| |
| |
| |
| /** @private */ |
| |
| private var _absoluteStart:int; // text-offset of start of line - from beginning of the TextFlow |
| private var _textLength:int; // number of chars to next line (incl trailing spaces, etc.) |
| private var _height:Number = 0; // y advance |
| CONFIG::debug |
| { |
| private var _spaceBefore:Number = 0; // amount of vertical space to leave at the top of the line |
| private var _spaceAfter:Number = 0; // amount of vertical space to leave at the bottom of the line |
| } |
| private var _x:Number = 0; // left edge of line |
| private var _y:Number = 0; // top edge of line |
| private var _outerTargetWidth:Number = 0; // width line is composed to, excluding indents |
| |
| private var _boundsLeftTW:int = 2; // text line bounds: left |
| private var _boundsRightTW:int = 1; // text line bounds: right (if left > right, then it is not set) |
| private var _boundsTopTW:int; // text line bounds: top |
| private var _boundsBottomTW:int; // text line bounds: bottom |
| |
| private var _para:ParagraphElement; // owning paragraph |
| private var _controller:ContainerController; // what frame the line was composed into |
| private var _columnIndex:int; // column number in the container |
| |
| private var _adornCount:int = 0; |
| |
| // added to support TextFlowLine when TextLine not available |
| private var _ascent:Number; |
| private var _descent:Number; |
| private var _targetWidth:Number; |
| private var _validity:String; |
| private var _textHeight:Number; |
| private var _lineOffset:Number; |
| private var _lineExtent:Number; // content bounds logical width for the line |
| private var _released:Boolean; // True if line has been released from the TextBlock |
| private var _alignment:String; // actual alignment applied to the line by composition (right or center), null if no alignment applied |
| private var _hasGraphicElement:Boolean; // True if line has as graphic element |
| |
| private var _textLineCache:WeakRef; |
| |
| /** @private */ |
| tlf_internal function get hasGraphicElement():Boolean |
| { return _hasGraphicElement; } |
| |
| /** |
| * The height of the text line, which is equal to <code>ascent</code> plus <code>descent</code>. The |
| * value is calculated based on the difference between the baselines that bound the line, either |
| * ideographic top and bottom or ascent and descent depending on whether the baseline at y=0 |
| * is ideographic (for example, TextBaseline.IDEOGRAPHIC_TOP) or not. |
| * |
| * @playerversion Flash 10 |
| * @playerversion AIR 1.5 |
| * @langversion 3.0 |
| * |
| * @see flash.text.engine.TextBaseline TextBaseline |
| */ |
| |
| public function get textHeight():Number |
| { return _textHeight; } |
| |
| /** @private */ |
| /** |
| * Check if the line is visible by comparing a set rectangle to the supplied |
| * rectangle (all values in Twips). |
| */ |
| tlf_internal function isLineVisible(wmode:String, x:int, y:int, w:int, h:int):Boolean |
| { |
| if (_boundsLeftTW > _boundsRightTW) |
| return false; |
| |
| if (wmode == BlockProgression.RL) |
| return _boundsRightTW >= x && _boundsLeftTW < x + w; |
| else |
| return _boundsBottomTW >= y && _boundsTopTW < y + h; |
| } |
| |
| /** @private |
| * Set the text line bounds rectangle, all values in Twips. |
| * If left > right, the rectangle is considered not to be set. |
| */ |
| tlf_internal function setLineBounds(x:int, y:int, w:int, h:int):void |
| { |
| _boundsLeftTW = x; |
| _boundsRightTW = x + w; |
| _boundsTopTW = y; |
| _boundsBottomTW = y + h; |
| } |
| |
| /** @private |
| * Check if the text line bounds are set. If the stored left |
| * value is > the right value, then the rectangle is not set. |
| */ |
| tlf_internal function hasLineBounds():Boolean |
| { |
| return (_boundsLeftTW <= _boundsRightTW); |
| } |
| |
| /** @private - the selection block cache */ |
| static private var _selectionBlockCache:Dictionary = new Dictionary(true); |
| |
| private static const EMPTY_LINE_WIDTH:Number = 2; // default size of empty line selection |
| |
| /** Constructor - creates a new TextFlowLine instance. |
| * <p><strong>Note</strong>: No client should call this. It's exposed for writing your own composer.</p> |
| * |
| * @param textLine The TextLine display object to use for this line. |
| * @param paragraph The paragraph (ParagraphElement) in which to place the line. |
| * @param outerTargetWidth The width the line is composed to, excluding indents. |
| * @param lineOffset The line's offset in pixels from the appropriate container inset (as dictated by paragraph direction and container block progression), prior to alignment of lines in the paragraph. |
| * @param absoluteStart The character position in the text flow at which the line begins. |
| * @param numChars The number of characters in the line. |
| * |
| * @playerversion Flash 10 |
| * @playerversion AIR 1.5 |
| * @langversion 3.0 |
| * |
| * @see flash.text.engine.TextLine |
| * @see ParagraphElement |
| * @see #absoluteStart |
| * @see #numChars |
| */ |
| |
| public function TextFlowLine(textLine:TextLine, paragraph:ParagraphElement, outerTargetWidth:Number = 0, lineOffset:Number = 0, absoluteStart:int = 0, numChars:int = 0) |
| { |
| initialize(paragraph, outerTargetWidth, lineOffset, absoluteStart,numChars,textLine); |
| } |
| |
| /** @private */ |
| tlf_internal function initialize(paragraph:ParagraphElement, outerTargetWidth:Number = 0, lineOffset:Number = 0, absoluteStart:int = 0, numChars:int = 0, textLine:TextLine = null):void |
| { |
| _para = paragraph; |
| _outerTargetWidth = outerTargetWidth; |
| _absoluteStart = absoluteStart; |
| _textLength = numChars; |
| _released = (textLine == null); |
| if (textLine) |
| { |
| _textLineCache = new WeakRef(textLine); |
| textLine.userData = this; |
| _targetWidth = textLine.specifiedWidth; |
| _ascent = textLine.ascent; |
| _descent = textLine.descent; |
| _textHeight = textLine.textHeight; |
| _lineOffset = lineOffset; |
| _validity = TextLineValidity.VALID; |
| _hasGraphicElement = textLine.hasGraphicElement; |
| } |
| else |
| _validity = TextLineValidity.INVALID; |
| } |
| |
| /** @private */ |
| tlf_internal function releaseTextLine():void |
| { _textLineCache = null; } |
| |
| /** @private */ |
| tlf_internal function peekTextLine():TextLine |
| { return _textLineCache ? _textLineCache.get() : null; } |
| |
| /** |
| * The horizontal position of the line relative to its container, expressed as the offset in pixels from the |
| * left of the container. |
| * <p><strong>Note: </strong>Although this property is technically <code>read-write</code>, |
| * you should treat it as <code>read-only</code>. The setter exists only to satisfy the |
| * requirements of the IVerticalJustificationLine interface that defines both a getter and setter for this property. |
| * Use of the setter, though possible, will lead to unpredictable results. |
| * </p> |
| * |
| * @playerversion Flash 10 |
| * @playerversion AIR 1.5 |
| * @langversion 3.0 |
| * |
| * @see #y |
| */ |
| public function get x():Number |
| { return _x; } |
| |
| /** |
| * This comment is ignored, but the setter should not be used and exists only to satisfy |
| * the IVerticalJustificationLine interface. |
| * @see flashx.textLayout.compose.IVerticalJustificationLine |
| * @private |
| */ |
| public function set x(lineX:Number):void |
| { |
| _x = lineX; |
| // invalidate bounds |
| _boundsLeftTW = 2; |
| _boundsRightTW = 1; |
| } |
| |
| /** |
| * The vertical position of the line relative to its container, expressed as the offset in pixels from the top |
| * of the container. |
| * <p><strong>Note: </strong>Although this property is technically <code>read-write</code>, |
| * you should treat it as <code>read-only</code>. The setter exists only to satisfy the |
| * requirements of the IVerticalJustificationLine interface that defines both a getter and setter for this property. |
| * Use of the setter, though possible, will lead to unpredictable results. |
| * </p> |
| * |
| * @playerversion Flash 10 |
| * @playerversion AIR 1.5 |
| * @langversion 3.0 |
| * |
| * @see #x |
| */ |
| public function get y():Number |
| { return _y; } |
| |
| /** This comment is ignored, but the setter should not be used and exists only to satisfy |
| * the IVerticalJustificationLine interface. |
| * @see flashx.textLayout.compose.IVerticalJustificationLine |
| * @private |
| */ |
| public function set y(lineY:Number):void |
| { |
| _y = lineY; |
| // invalidate bounds |
| _boundsLeftTW = 2; |
| _boundsRightTW = 1; |
| } |
| |
| /** @private */ |
| tlf_internal function setXYAndHeight(lineX:Number,lineY:Number,lineHeight:Number):void |
| { |
| _x = lineX; |
| _y = lineY; |
| _height = lineHeight; |
| // invalidate bounds |
| _boundsLeftTW = 2; |
| _boundsRightTW = 1; |
| } |
| |
| /** |
| * One of the values from TextFlowLineLocation for specifying a line's location within a paragraph. |
| * |
| * @playerversion Flash 10 |
| * @playerversion AIR 1.5 |
| * @langversion 3.0 |
| * |
| * @see ParagraphElement |
| * @see TextFlowLineLocation |
| */ |
| |
| public function get location():int |
| { |
| if (_para) |
| { |
| var lineStart:int = _absoluteStart - _para.getAbsoluteStart(); |
| |
| // Initialize settings for location |
| if (lineStart == 0) // we're at the start of the paragraph |
| return _textLength == _para.textLength ? TextFlowLineLocation.ONLY : TextFlowLineLocation.FIRST; |
| if (lineStart + _textLength == _para.textLength) // we're at the end of the para |
| return TextFlowLineLocation.LAST; |
| } |
| return TextFlowLineLocation.MIDDLE; |
| } |
| |
| /** |
| * The controller (ContainerController object) for the container in which the line has been placed. |
| * |
| * @playerversion Flash 10 |
| * @playerversion AIR 1.5 |
| * @langversion 3.0 |
| * |
| * @see flashx.textLayout.container.ContainerController |
| */ |
| |
| public function get controller():ContainerController |
| { return _controller; } |
| |
| /** The number of the column in which the line has been placed, with the first column being 0. |
| * |
| * @playerversion Flash 10 |
| * @playerversion AIR 1.5 |
| * @langversion 3.0 |
| */ |
| |
| public function get columnIndex():int |
| { return _columnIndex; } |
| |
| /** @private */ |
| tlf_internal function setController(cont:ContainerController,colNumber:int):void |
| { |
| _controller = cont as ContainerController; |
| _columnIndex = colNumber; |
| } |
| |
| /** The height of the line in pixels. |
| * |
| * @playerversion Flash 10 |
| * @playerversion AIR 1.5 |
| * @langversion 3.0 |
| * |
| * @see #width |
| */ |
| |
| public function get height():Number |
| { return _height; } |
| |
| /** |
| * @copy flash.text.engine.TextLine#ascent |
| * |
| * @playerversion Flash 10 |
| * @playerversion AIR 1.5 |
| * @langversion 3.0 |
| */ |
| |
| public function get ascent():Number |
| { return _ascent; } |
| |
| /** |
| * @copy flash.text.engine.TextLine#descent |
| * |
| * @playerversion Flash 10 |
| * @playerversion AIR 1.5 |
| * @langversion 3.0 |
| */ |
| |
| public function get descent():Number |
| { return _descent; } |
| |
| /** |
| * The line's offset in pixels from the appropriate container inset (as dictated by paragraph direction and container block progression), |
| * prior to alignment of lines in the paragraph. |
| * |
| * @playerversion Flash 10 |
| * @playerversion AIR 1.5 |
| * @langversion 3.0 |
| */ |
| public function get lineOffset():Number |
| { |
| return _lineOffset; |
| } |
| |
| |
| /** |
| * The paragraph (ParagraphElement) in which the line resides. |
| * |
| * @playerversion Flash 10 |
| * @playerversion AIR 1.5 |
| * @langversion 3.0 |
| * |
| * @see ParagraphElement |
| */ |
| |
| public function get paragraph():ParagraphElement |
| { return _para; } |
| |
| /** |
| * The location of the line as an absolute character position in the TextFlow object. |
| * |
| * @return the character position in the text flow at which the line begins. |
| * |
| * @playerversion Flash 10 |
| * @playerversion AIR 1.5 |
| * @langversion 3.0 |
| * |
| * @see TextFlow |
| */ |
| |
| public function get absoluteStart():int |
| { return _absoluteStart; } |
| /** @private */ |
| tlf_internal function setAbsoluteStart(val:int):void |
| { _absoluteStart = val; } |
| |
| /** |
| * The number of characters to the next line, including trailing spaces. |
| * |
| * @playerversion Flash 10 |
| * @playerversion AIR 1.5 |
| * @langversion 3.0 |
| */ |
| |
| public function get textLength():int |
| { return _textLength; } |
| /** @private */ |
| tlf_internal function setTextLength(val:int):void |
| { |
| _textLength = val; |
| // assert(_validity == TextLineValidity.INVALID, "not already damaged"); |
| damage(TextLineValidity.INVALID); |
| } |
| |
| /** |
| * The amount of space to leave before the line. |
| * <p>If the line is the first line of a paragraph that has a space-before applied, the line will have |
| * a <code>spaceBefore</code> value. If the line comes at the top of a column, <code>spaceBefore</code> is ignored. |
| * Otherwise, the line follows another line in the column, and it is positioned vertically to insure that there is |
| * at least this much space left between this line and the last line of the preceding paragraph.</p> |
| * |
| * @playerversion Flash 10 |
| * @playerversion AIR 1.5 |
| * @langversion 3.0 |
| * |
| * @see flashx.textLayout.formats.TextLayoutFormat#paragraphSpaceBefore TextLayoutFormat.paragraphSpaceBefore |
| */ |
| |
| public function get spaceBefore():Number |
| { |
| CONFIG::debug |
| { |
| var newResult:Number = (this.location & TextFlowLineLocation.FIRST)? _para.computedFormat.paragraphSpaceBefore : 0; |
| assert(newResult == _spaceBefore, "spaceBefore getting wrong result"); |
| } |
| return (this.location & TextFlowLineLocation.FIRST)? _para.computedFormat.paragraphSpaceBefore : 0; |
| } |
| |
| /** @private */ |
| CONFIG::debug tlf_internal function setSpaceBefore(val:Number):void |
| { _spaceBefore = val; } |
| |
| /** |
| * The amount of space to leave after the line. |
| * <p>If the line is the last line of a paragraph that has a space-after, the line will have |
| * a <code>spaceAfter</code> value. If the line comes at the bottom of a column, then the <code>spaceAfter</code> |
| * is ignored. Otherwise, the line comes before another line in the column, and the following line must be positioned vertically to |
| * insure that there is at least this much space left between this last line of the paragraph and the first |
| * line of the following paragraph.</p> |
| * |
| * @playerversion Flash 10 |
| * @playerversion AIR 1.5 |
| * @langversion 3.0 |
| * |
| * @see flashx.textLayout.formats.TextLayoutFormat#paragraphSpaceAfter TextLayoutFormat.paragraphSpaceAfter |
| */ |
| |
| public function get spaceAfter():Number |
| { |
| CONFIG::debug |
| { |
| var newResult:Number = (this.location & TextFlowLineLocation.LAST) ? _para.computedFormat.paragraphSpaceAfter : 0; |
| assert(newResult == _spaceAfter, "spaceAfter getting wrong result"); |
| } |
| return ((this.location & TextFlowLineLocation.LAST) ? _para.computedFormat.paragraphSpaceAfter : 0); |
| } |
| |
| /** @private */ |
| CONFIG::debug tlf_internal function setSpaceAfter(val:Number):void |
| { _spaceAfter = val ; } |
| |
| /** @private |
| * Target width not including paragraph indents */ |
| tlf_internal function get outerTargetWidth():Number |
| { return _outerTargetWidth; } |
| |
| /** @private */ |
| tlf_internal function set outerTargetWidth(val:Number):void |
| { _outerTargetWidth = val; } |
| |
| /** @private |
| * Amount of space used to break the line |
| * <p>The target width is the amount of space allowed for the line, including the space required for indents.</p> |
| */ |
| tlf_internal function get targetWidth():Number |
| { return _targetWidth; } |
| |
| /** |
| * Returns the bounds of the line as a rectangle. |
| * |
| * @return a rectangle that represents the boundaries of the line. |
| * |
| * @playerversion Flash 10 |
| * @playerversion AIR 1.5 |
| * @langversion 3.0 |
| */ |
| |
| public function getBounds():Rectangle |
| { |
| var textLine:TextLine = getTextLine(true); |
| if (!textLine) |
| return new Rectangle(); |
| |
| // TODO: just use the textLine.x and textLine.y - after all getTextLine now sets them. |
| // not going to change this right now though |
| var bp:String = paragraph.getAncestorWithContainer().computedFormat.blockProgression; |
| var shapeX:Number = createShapeX(); |
| var shapeY:Number = createShapeY(bp); |
| if (bp == BlockProgression.TB) |
| shapeY += descent-textLine.height; |
| return new Rectangle(shapeX, shapeY, textLine.width, textLine.height); |
| } |
| |
| /** The validity of the line. |
| * <p>A line can be invalid if the text, the attributes applied to it, or the controller settings have |
| * changed since the line was created. An invalid line can still be displayed, and you can use it, but the values |
| * used will be the values at the time it was created. The line represented by <code>textLine</code> also will be in an |
| * invalid state. </p> |
| * |
| * @playerversion Flash 10 |
| * @playerversion AIR 1.5 |
| * @langversion 3.0 |
| * |
| * @see #textLine |
| * @see flash.text.engine.TextLine#validity TextLine.validity |
| * @see #GEOMETRY_DAMAGED |
| */ |
| |
| public function get validity():String |
| { |
| // A TextLine may be invalidated separately from the TextFlowLine, when the invalidation is driven from the Player (e.g. changes have been made directly). |
| // If the TextFlowLine is marked valid, the line may still be invalid if the TextLine has been marked invalid. |
| // If the line has been released (TextBlock.releaseLines called), then it may have an existing TextLine that got marked invalid by the Player |
| // when it was released. We want to ignore that invalid marking. |
| if (!_released) |
| { |
| var textLine:TextLine = peekTextLine(); |
| if (textLine && (_validity == FlowDamageType.GEOMETRY || _validity == TextLineValidity.VALID) && textLine.validity != TextLineValidity.VALID) |
| _validity = textLine.validity; |
| } |
| return _validity; |
| } |
| |
| /** |
| * The width of the line if it was not justified. For unjustified text, this value is the same as <code>textLength</code>. |
| * For justified text, this value is what the length would have been without justification, and <code>textLength</code> |
| * represents the actual line width. For example, when the following String is justified and assigned a width of 500, it |
| * has an actual width of 500 but an unjustified width of 268.9921875. |
| * |
| * <p>TBD: add graphic of justified line </p> |
| * |
| * @playerversion Flash 10 |
| * @playerversion AIR 1.5 |
| * @langversion 3.0 |
| */ |
| |
| public function get unjustifiedTextWidth():Number |
| { |
| // hack - outerTargetWidth holds value from the factory |
| var textLine:TextLine = getTextLine(true); |
| return textLine.unjustifiedTextWidth + (_outerTargetWidth - targetWidth); |
| } |
| |
| /** @private */ |
| tlf_internal function get lineExtent():Number |
| { |
| return _lineExtent; |
| } |
| |
| /** @private */ |
| tlf_internal function set lineExtent(value:Number):void |
| { |
| _lineExtent = value; |
| } |
| |
| /** @private */ |
| tlf_internal function get alignment():String |
| { |
| return _alignment; |
| } |
| |
| /** @private */ |
| tlf_internal function set alignment(value:String):void |
| { |
| _alignment = value; |
| } |
| |
| /** @private |
| * True if the line needs composing. */ |
| tlf_internal function isDamaged():Boolean |
| { |
| if (_validity != TextLineValidity.VALID) |
| return true; |
| if (!_released) |
| { |
| var textLine:TextLine = peekTextLine(); |
| if (textLine && textLine.validity != TextLineValidity.VALID) |
| return true; |
| } |
| return false; |
| } |
| |
| /** @private |
| * Mark the line as valid */ |
| tlf_internal function clearDamage():void |
| { |
| CONFIG::debug { assert(_validity == FlowDamageType.GEOMETRY, "can't clear damage other than geometry"); } |
| if (_validity == TextLineValidity.VALID) // already is valid |
| return; |
| _validity = TextLineValidity.VALID; |
| |
| //CONFIG::debug { assert(_textLineCache != null, "bad call to clearDamage"); } |
| |
| var textLine:TextLine = peekTextLine(); |
| |
| // The line in the cache, if there is one, is either invalid because its been released, or its geometry_damaged, or its already valid. |
| CONFIG::debug { assert(!textLine || _released || textLine.validity == TextLineValidity.VALID || textLine.validity == FlowDamageType.GEOMETRY, "can't clear TextLine damage other than geometry"); } |
| |
| if (textLine && !_released) // mark the TextLine as well |
| { |
| textLine.validity = TextLineValidity.VALID; |
| CONFIG::debug { Debugging.traceFTEAssign(textLine,"validity",TextLineValidity.VALID); } |
| } |
| } |
| |
| /** @private |
| * Mark the line as damaged */ |
| |
| tlf_internal function damage(damageType:String):void |
| { |
| // trace("TextFlowLine.damage ", this.start.toString(), this.textLength.toString()); |
| if (_validity == damageType || _validity == TextLineValidity.INVALID) |
| return; // totally damaged |
| _validity = damageType; |
| |
| var textLine:TextLine = peekTextLine(); |
| if (textLine && textLine.validity != TextLineValidity.INVALID) |
| { |
| textLine.validity = _validity; |
| CONFIG::debug { Debugging.traceFTEAssign(textLine,"validity",damageType); } |
| } |
| } |
| |
| /** @private */ |
| CONFIG::debug public function toString():String |
| { |
| return "x:" + x + " y: " + y + " absoluteStart:" + absoluteStart + " textLength:" + textLength + " location: " + location + " validity: " + _validity; |
| } |
| |
| /** |
| * Indicates whether the <code>flash.text.engine.TextLine</code> object for this TextFlowLine exists. |
| * The value is <code>true</code> if the TextLine object has <em>not</em> been garbage collected and |
| * <code>false</code> if it has been. |
| * |
| * @playerversion Flash 10 |
| * @playerversion AIR 1.5 |
| * @langversion 3.0 |
| * |
| * @see flash.text.engine.TextLine TextLine |
| */ |
| |
| public function get textLineExists():Boolean |
| { |
| return peekTextLine() != null; |
| } |
| |
| /** |
| * Returns the <code>flash.text.engine.TextLine</code> object for this line, which might be recreated |
| * if it does not exist due to garbage collection. Set <code>forceValid</code> to <code>true</code> |
| * to cause the TextLine to be regenerated. Returns null if the TextLine cannot be recreated. |
| *. |
| * @param forceValid if true, the TextLine is regenerated, if it exists but is invalid. |
| * |
| * @return object for this line or <code>null</code> if the TextLine object cannot be |
| * recreated. |
| * |
| * @playerversion Flash 10 |
| * @playerversion AIR 1.5 |
| * @langversion 3.0 |
| * |
| * @see flash.text.engine.TextLine TextLine |
| */ |
| |
| public function getTextLine(forceValid:Boolean = false):TextLine |
| { |
| var textLine:TextLine = peekTextLine(); |
| if (!textLine || (textLine.validity != TextLineValidity.VALID && forceValid)) |
| { |
| if (isDamaged() && validity != FlowDamageType.GEOMETRY) |
| return null; |
| |
| var textBlock:TextBlock = paragraph.getTextBlock(); |
| |
| // regenerate the whole paragraph at once, up to current position. The TextBlock may already contain valid |
| // lines that got generated on a prior call to getTextLine but couldn't be added to the cache (e.g., because |
| // the cache contains an invalid line that is in the display list), so we check for that before making a new line. |
| var previousLine:TextLine; |
| var currentLine:TextLine = textBlock.firstLine; |
| var flowComposer:IFlowComposer = paragraph.getTextFlow().flowComposer; |
| var lineIndex:int = flowComposer.findLineIndexAtPosition(paragraph.getAbsoluteStart()); |
| do |
| { |
| var line:TextFlowLine = flowComposer.getLineAt(lineIndex); |
| CONFIG::debug { assert (line.paragraph == paragraph, "Expecting line in same paragraph"); } |
| if (currentLine != null && currentLine.validity == TextLineValidity.VALID) |
| { |
| textLine = currentLine; |
| currentLine = currentLine.nextLine; |
| |
| line.updateTextLineCache(textLine); |
| } |
| else |
| { |
| textLine = line.recreateTextLine(textBlock, previousLine); |
| currentLine = null; |
| } |
| previousLine = textLine; |
| ++lineIndex; |
| } while (line != this); |
| } |
| |
| if(textLine != null && textLine.numChildren == 0 && _adornCount > 0) |
| { |
| var para:ParagraphElement = this.paragraph; |
| var paraStart:int = para.getAbsoluteStart(); |
| var elem:FlowLeafElement = para.findLeaf(this.absoluteStart - paraStart); |
| var elemStart:int = elem.getAbsoluteStart(); |
| |
| createAdornments(para.getAncestorWithContainer().computedFormat.blockProgression,elem, elemStart); |
| } |
| |
| return textLine; |
| } |
| |
| /** @private Regenerate the TextLine -- called when textLine has been gc'ed */ |
| tlf_internal function recreateTextLine(textBlock:TextBlock, previousLine:TextLine):TextLine |
| { |
| var textLine:TextLine; |
| |
| // If we already have a valid text line, just return it. |
| if (!_released) |
| { |
| textLine = peekTextLine(); |
| if (textLine) |
| return textLine; |
| } |
| |
| var textFlow:TextFlow = paragraph.getTextFlow(); |
| var flowComposer:IFlowComposer = textFlow.flowComposer; |
| var swfContext:ISWFContext = flowComposer.swfContext ? flowComposer.swfContext : BaseCompose.globalSWFContext; |
| |
| textLine = TextLineRecycler.getLineForReuse(); |
| if (textLine) |
| { |
| CONFIG::debug { assert(textFlow.backgroundManager == null || textFlow.backgroundManager.lineDict[textLine] === undefined,"Bad TextLine in recycler cache"); } |
| textLine = swfContext.callInContext(textBlock["recreateTextLine"], textBlock, [ textLine, previousLine, _targetWidth, _lineOffset, true ]); |
| } |
| else |
| textLine = swfContext.callInContext(textBlock.createTextLine, textBlock, [ previousLine, _targetWidth, _lineOffset, true ]); |
| |
| textLine.x = createShapeX(); |
| CONFIG::debug { Debugging.traceFTEAssign(textLine,"x", createShapeX()); } |
| textLine.y = createShapeY(textFlow.computedFormat.blockProgression); |
| CONFIG::debug { Debugging.traceFTEAssign(textLine,"y", createShapeY(textFlow.computedFormat.blockProgression)); } |
| textLine.doubleClickEnabled = true; |
| |
| updateTextLineCache(textLine); |
| |
| return textLine; |
| } |
| |
| /** the rule is that all "displayed" lines must be in the TextFlowLine textLineCache. Put this new line in the cache iff there isn't already a displayed line */ |
| private function updateTextLineCache(textLine:TextLine):void |
| { |
| textLine.userData = this; |
| var existingTextLine:TextLine = peekTextLine(); |
| // If there is an existing, released line, and it is currently being displayed, we can't replace it in the cache. |
| if (!existingTextLine || existingTextLine.parent == null) |
| { |
| if (existingTextLine != textLine) |
| _textLineCache = new WeakRef(textLine); |
| _released = false; |
| } |
| } |
| |
| /** @private */ |
| tlf_internal function markReleased():void |
| { |
| _released = true; |
| } |
| |
| /** @private */ |
| tlf_internal function createShape(bp:String):TextLine |
| { |
| var textLine:TextLine = getTextLine(); |
| |
| var newX:Number = createShapeX(); |
| //if (int(newX*20) != int(textLine.x*20)) |
| { |
| textLine.x = newX; |
| CONFIG::debug { Debugging.traceFTEAssign(textLine,"x", newX); } |
| } |
| var newY:Number = createShapeY(bp); |
| //if (int(newY*20) != int(textLine.y*20)) |
| { |
| textLine.y = newY; |
| CONFIG::debug { Debugging.traceFTEAssign(textLine,"y", newY); } |
| } |
| return textLine; |
| } |
| |
| private function createShapeX():Number |
| { return x; } |
| |
| private function createShapeY(bp:String):Number |
| { return bp == BlockProgression.RL ? y : y + _ascent; } |
| |
| /** @private |
| * Scan through the format runs within the line, and draw any underline or strikethrough that might need it |
| */ |
| |
| tlf_internal function createAdornments(blockProgression:String,elem:FlowLeafElement,elemStart:int):void |
| { |
| CONFIG::debug { assert(elemStart == elem.getAbsoluteStart(),"bad elemStart passed to createAdornments"); } |
| var endPos:int = _absoluteStart + _textLength; |
| |
| //init adornments back to 0 |
| _adornCount = 0; |
| |
| for (;;) |
| { |
| var format:ITextLayoutFormat = elem.computedFormat; |
| |
| _adornCount += elem.updateAdornments(this, blockProgression); |
| |
| var fvh:FlowValueHolder = elem.format as FlowValueHolder; |
| if(fvh && fvh.userStyles && fvh.userStyles.imeStatus) |
| { |
| elem.updateIMEAdornments(this, blockProgression, fvh.userStyles.imeStatus as String); |
| } |
| elemStart += elem.textLength; |
| if (elemStart >= endPos) |
| break; |
| elem = elem.getNextLeaf(_para); |
| CONFIG::debug { assert(elem != null,"bad nextLeaf"); } |
| } |
| } |
| |
| /** @private |
| * Scan through the format runs within the line, and figure out what the leading for the overall line is. |
| * The line's leading is equal to the maximum leading of any individual run within the line. |
| * The leading in an individual format run is calculated by looking at the leading attribute in the |
| * CharacterFormat. If it is set to a value, we just use that value. Otherwise, if it is set to AUTO, |
| * we calculate the leading based on the point size and the auto leading percentage from the ParagraphFormat. |
| */ |
| |
| tlf_internal function getLineLeading(bp:String,elem:FlowLeafElement,elemStart:int):Number |
| { |
| CONFIG::debug { assert(elemStart == elem.getAbsoluteStart(),"bad elemStart passed to getLineLeading"); } |
| var endPos:int = _absoluteStart + _textLength; |
| var totalLeading:Number = 0; |
| CONFIG::debug { assert(elem.getAncestorWithContainer() != null,"element with no container"); } |
| for (;;) |
| { |
| //this is kinda bunk and really shouldn't be here, but I'm loath to find a better way.... |
| //ignore the leading on a TCY Block. |
| //if the elem is in a TCYBlock, AND it is not the only block in the line, "skip" it |
| if (!(bp == BlockProgression.RL && (elem.parent is TCYElement) && (!isNaN(totalLeading) || (elem.textLength != this.textLength)))) |
| { |
| var elemLeading:Number = TextLayoutFormat.lineHeightProperty.computeActualPropertyValue(elem.computedFormat.lineHeight,elem.getEffectiveFontSize()); |
| totalLeading = Math.max(totalLeading, elemLeading); |
| } |
| elemStart += elem.textLength; |
| if (elemStart >= endPos) |
| break; |
| elem = elem.getNextLeaf(_para); |
| CONFIG::debug { assert(elem != null,"bad nextLeaf"); } |
| } |
| return totalLeading; |
| } |
| |
| /** @private |
| * Scan through the format runs within the line, and figure out what the typographic ascent (i.e. ascent relative to the |
| * Roman baseline) for the overall line is. Normally it is the distance between the Roman and Ascent baselines, |
| * but it may be adjusted upwards by the width/height of the GraphicElement. |
| */ |
| tlf_internal function getLineTypographicAscent(elem:FlowLeafElement,elemStart:int):Number |
| { |
| CONFIG::debug { assert(elemStart == elem.getAbsoluteStart(),"bad elemStart passed to getLineTypographicAscent"); } |
| return getTextLineTypographicAscent(getTextLine(), elem, elemStart, absoluteStart+textLength, _para); |
| } |
| |
| /** @private |
| * Scan through the format runs within the line, and figure out what the typographic ascent (i.e. ascent relative to the |
| * Roman baseline) for the overall line is. Normally it is the distance between the Roman and Ascent baselines, |
| * but it may be adjusted upwards by the width/height of the GraphicElement. |
| */ |
| static tlf_internal function getTextLineTypographicAscent(textLine:TextLine, elem:FlowLeafElement,elemStart:int, textLineEnd:int, para:ParagraphElement):Number |
| { |
| CONFIG::debug { assert(elemStart == elem.getAbsoluteStart(),"bad elemStart passed to getTextLineTypographicAscent"); } |
| var rslt:Number = textLine.getBaselinePosition(flash.text.engine.TextBaseline.ROMAN) - textLine.getBaselinePosition(flash.text.engine.TextBaseline.ASCENT); |
| |
| for (;;) |
| { |
| if (elem is InlineGraphicElement) |
| rslt = Math.max(rslt,InlineGraphicElement(elem).getTypographicAscent(textLine)); |
| elemStart += elem.textLength; |
| if (elemStart >= textLineEnd) |
| break; |
| elem = elem.getNextLeaf(para); |
| CONFIG::debug { assert(elem != null,"bad nextLeaf"); } |
| } |
| return rslt; |
| } |
| |
| //helper method to determine which subset of line is underlined |
| //I believe this will be replaced by the eventSink mechanism |
| private function isTextlineSubsetOfSpan(element:FlowLeafElement): Boolean |
| { |
| var spanStart:int = element.getAbsoluteStart(); |
| var spanEnd:int = spanStart + element.textLength; |
| |
| var lineStart:int = this.absoluteStart; |
| var lineEnd:int = this.absoluteStart + this._textLength; |
| |
| return spanStart <= lineStart && spanEnd >= lineEnd; |
| } |
| |
| |
| |
| /** Create a rectangle for selection */ |
| static private function createSelectionRect(selObj:Shape, color:uint, x:Number, y:Number, width:Number, height:Number):DisplayObject |
| { |
| selObj.graphics.beginFill(color); |
| var cmds:Vector.<int> = new Vector.<int>(); |
| var pathPoints:Vector.<Number> = new Vector.<Number>(); |
| |
| //set the start point - topLeft |
| cmds.push(GraphicsPathCommand.MOVE_TO); |
| pathPoints.push(x); |
| pathPoints.push(y); |
| |
| //line to topRight |
| cmds.push(GraphicsPathCommand.LINE_TO); |
| pathPoints.push(x + width); |
| pathPoints.push(y); |
| |
| //line to botRight |
| cmds.push(GraphicsPathCommand.LINE_TO); |
| pathPoints.push(x + width); |
| pathPoints.push(y + height); |
| |
| //line to botLeft |
| cmds.push(GraphicsPathCommand.LINE_TO); |
| pathPoints.push(x); |
| pathPoints.push(y + height); |
| |
| //line to close the path - topLeft |
| cmds.push(GraphicsPathCommand.LINE_TO); |
| pathPoints.push(x); |
| pathPoints.push(y); |
| |
| selObj.graphics.drawPath(cmds, pathPoints, flash.display.GraphicsPathWinding.NON_ZERO); |
| |
| return selObj; |
| } |
| |
| /** @private getSelectionShapesCacheEntry |
| * |
| * creates and adds block selection(s) to the text container. In most circumstances, |
| * this method will produce and add a single DisplayObject, but in certain circumstances, |
| * such as TCY in TTB text, will need to make multiple selection rectangles. |
| * |
| * Examples: |
| * 1) horizontal - ABCDE |
| * 2) vertical - ABCDE |
| * 3) horizontal - ABcdE |
| * 4) vertical: A |
| * B |
| * cde |
| * F |
| * |
| */ |
| private function getSelectionShapesCacheEntry(begIdx:int, endIdx:int, prevLine:TextFlowLine, nextLine:TextFlowLine, blockProgression:String):SelectionCache |
| { |
| if (isDamaged()) |
| return null; |
| |
| // CONFIG::debug { assert(_textLineCache != null, "bad call to getSelectionShapesCacheEntry"); } |
| |
| //get the absolute start of the paragraph. Calculation is expensive, so just do this once. |
| var paraAbsStart:int = _para.getAbsoluteStart(); |
| |
| //if the indexes are identical and are equal to the start of the line, then |
| //don't draw anything. This prevents a bar being drawn on a following line when |
| //selecting accross line boundaries |
| //with exception for a selection that includes just the first character of an empty last line in the TextFlow |
| if (begIdx == endIdx && paraAbsStart + begIdx == absoluteStart) |
| { |
| if (absoluteStart != _para.getTextFlow().textLength-1) |
| return null; |
| endIdx++; |
| } |
| |
| //the cached selection bounds and rects |
| var selectionCache:SelectionCache = _selectionBlockCache[this]; |
| if (selectionCache && selectionCache.begIdx == begIdx && selectionCache.endIdx == endIdx) |
| return selectionCache; |
| |
| var drawRects:Array = new Array(); |
| //an array to store any tcy rectangles which need separate processing and may not exist |
| var tcyDrawRects:Array = new Array(); |
| |
| if(selectionCache == null) |
| { |
| selectionCache = new SelectionCache(); |
| _selectionBlockCache[this] = selectionCache; |
| } |
| else |
| { |
| selectionCache.clear(); |
| } |
| selectionCache.begIdx = begIdx; |
| selectionCache.endIdx = endIdx; |
| |
| var textLine:TextLine = getTextLine(); |
| var heightAndAdj:Array = getRomanSelectionHeightAndVerticalAdjustment(prevLine, nextLine); |
| calculateSelectionBounds(textLine, drawRects, begIdx, endIdx, blockProgression, heightAndAdj); |
| |
| //iterate the blocks and create DisplayObjects to draw... |
| for each(var drawRect:Rectangle in drawRects) |
| { |
| CONFIG::debug{ assert(selectionCache != null, "If we're caching, selectionArray should never be null!"); } |
| //we have to make new rectangles or the convertLineRectToGlobal will alter the cached ones! |
| selectionCache.pushSelectionBlock(drawRect); |
| } |
| |
| |
| //allow the atoms to be garbage collected. |
| if (textLine) |
| textLine.flushAtomData(); |
| |
| return selectionCache; |
| } |
| |
| |
| /** @private - helper method to calculate all selection blocks within a line.*/ |
| tlf_internal function calculateSelectionBounds(textLine:TextLine, rectArray:Array, begIdx:int, endIdx:int, blockProgression:String, heightAndAdj:Array):void |
| { |
| //the direction of the text |
| var direction:String = _para.computedFormat.direction; |
| //get the absolute start of the paragraph. Calculation is expensive, so just do this once. |
| var paraAbsStart:int = _para.getAbsoluteStart(); |
| //the current index. used to iterate to the next element |
| var curIdx:int = begIdx; |
| //the current FlowLeafElement as determined by curIdx |
| var curElem:FlowLeafElement = null; |
| //the hightest glyph. Needed to normalize the rectangles we'll be building |
| var largestRise:Number = 0; |
| |
| //blockRectArray holds each leaf's blocks which could be 1 or more |
| var blockRectArray:Array = new Array(); |
| //floatRectArray holds the selection rects for any floats in the range. |
| var floatRectArray:Array = null; |
| //tcyDrawRects:Array |
| var tcyDrawRects:Array = null; |
| |
| //do this loop and only afterwards perform the normalization and addition to the rectArr |
| while(curIdx < endIdx) |
| { |
| curElem = _para.findLeaf(curIdx); |
| //if we somehow got a 0 length element, then increment the index and continue |
| if(curElem.textLength == 0) |
| { |
| ++curIdx; |
| continue; |
| } |
| else if(curElem is InlineGraphicElement && (curElem as InlineGraphicElement).float != Float.NONE) |
| { |
| if(floatRectArray == null) |
| floatRectArray = new Array(); |
| |
| var tempFloatArray:Array = makeSelectionBlocks(curIdx, curIdx+1, paraAbsStart, blockProgression, direction, heightAndAdj); |
| CONFIG::debug{ assert(tempFloatArray.length == 1, "How can a single floated InlineGraph have multiple shapes!"); } |
| floatRectArray.push(tempFloatArray[0]); |
| ++curIdx; |
| continue; |
| } |
| //the number of potential glyphs to hilite. Could larger than needs be if we are only selecting part of it. |
| var numCharsSelecting:int = curElem.textLength + curElem.getElementRelativeStart(_para) - curIdx; |
| //the index of the last glyph to hilite. If a partial selection, use endIdx |
| var endPos:int = (numCharsSelecting + curIdx) > endIdx ? endIdx : (numCharsSelecting + curIdx); |
| |
| //if this is not a TCY in vertical, the blocks should all be running in the same direction |
| if (blockProgression != BlockProgression.RL || |
| (textLine.getAtomTextRotation(textLine.getAtomIndexAtCharIndex(curIdx)) != TextRotation.ROTATE_0)) |
| { |
| var leafBlockArray:Array = makeSelectionBlocks(curIdx, endPos, paraAbsStart, blockProgression, direction, heightAndAdj); |
| //copy all the blocks into the blockRectArray - we'll normalize them later |
| for(var leafBlockIter:int = 0; leafBlockIter < leafBlockArray.length; ++leafBlockIter) |
| { |
| blockRectArray.push(leafBlockArray[leafBlockIter]); |
| } |
| } |
| else |
| { |
| var tcyBlock:FlowElement = curElem.getParentByType(TCYElement); |
| CONFIG::debug{ assert(tcyBlock != null, "What kind of object is this that is ROTATE_0, but not TCY?");} |
| |
| //if this element is still encompassed by a SubParagraphGroupElement of some kind (either a link or a TCYBlock) |
| //keep moving up to the parent. Otherwise, the below code will go into an infinite loop. bug 1905734 |
| var tcyParentRelativeEnd:int = tcyBlock.parentRelativeEnd; |
| var subParBlock:SubParagraphGroupElement = tcyBlock.getParentByType(SubParagraphGroupElement) as SubParagraphGroupElement; |
| while (subParBlock) |
| { |
| tcyParentRelativeEnd += subParBlock.parentRelativeStart; |
| subParBlock = subParBlock.getParentByType(SubParagraphGroupElement) as SubParagraphGroupElement; |
| } |
| |
| var largestTCYRise:Number = 0; |
| var lastTCYIdx:int = endIdx < tcyParentRelativeEnd ? endIdx : tcyParentRelativeEnd; |
| var tcyRects:Array = new Array(); |
| |
| |
| while(curIdx < lastTCYIdx) |
| { |
| curElem = _para.findLeaf(curIdx); |
| numCharsSelecting = curElem.textLength + curElem.getElementRelativeStart(_para) - curIdx; |
| endPos = numCharsSelecting + curIdx > endIdx ? endIdx : numCharsSelecting + curIdx; |
| var tcyRectArray:Array = makeSelectionBlocks(curIdx, endPos, paraAbsStart, blockProgression, direction, heightAndAdj); |
| |
| for(var tcyBlockIter:int = 0; tcyBlockIter < tcyRectArray.length; ++tcyBlockIter) |
| { |
| var tcyRect:Rectangle = tcyRectArray[tcyBlockIter]; |
| |
| if(tcyRect.height > largestTCYRise) |
| { |
| largestTCYRise = tcyRect.height; |
| } |
| |
| tcyRects.push(tcyRect); |
| } |
| curIdx = endPos; |
| } |
| |
| if(!tcyDrawRects) |
| tcyDrawRects = new Array(); |
| |
| normalizeRects(tcyRects, tcyDrawRects, largestTCYRise, BlockProgression.TB, direction); |
| continue; |
| |
| } |
| |
| //set the curIdx to the last char in the block |
| curIdx = endPos; |
| } |
| |
| |
| |
| //adding check for an empty set of draw rects. If there are not recangles, skip this. |
| //this can happen is there are ONLY TCY blocks and the whole line is selected. |
| //Watson 2273832. - gak 02.09.09 |
| //if the whole line is selected |
| if(blockRectArray.length > 0 && (paraAbsStart + begIdx) == absoluteStart && (paraAbsStart + endIdx) == (absoluteStart + textLength)) |
| { |
| curElem = _para.findLeaf(begIdx); |
| //if we have the entire line selected, but the first element is NOT the last, then |
| //we will land up with a selection which is 1 character wider than it should be. |
| if(((curElem.getAbsoluteStart() + curElem.textLength) < (absoluteStart + textLength)) && endPos >= 2) |
| { |
| //make sure that this is a white char and that we aren't deselecting the last |
| //char in a line - esp important for scripts which don't use spaces ie Japanese |
| var charCode:int = _para.getCharCodeAtPosition(endPos - 1); |
| if(charCode != SpanElement.kParagraphTerminator.charCodeAt(0) && CharacterUtil.isWhitespace(charCode)) |
| { |
| var lastElemBlockArray:Array = makeSelectionBlocks(endPos - 1, endPos - 1, paraAbsStart, blockProgression, direction, heightAndAdj); |
| var lastRect:Rectangle = lastElemBlockArray[lastElemBlockArray.length - 1]; |
| var modifyRect:Rectangle = blockRectArray[blockRectArray.length - 1] as Rectangle; |
| |
| if (blockProgression != BlockProgression.RL) |
| { |
| //if they have the same width, simply remove the last block |
| if(modifyRect.width == lastRect.width) |
| { |
| blockRectArray.pop(); |
| } |
| else |
| { |
| modifyRect.width -= lastRect.width; |
| |
| //if this is RTL, we need to shift the selection block over by the amount |
| //we reduced it. |
| if(direction == Direction.RTL) |
| modifyRect.left -= lastRect.width; |
| } |
| } |
| else |
| { |
| //if they have the same height, simply remove the last block |
| if(modifyRect.height == lastRect.height) |
| { |
| blockRectArray.pop(); |
| } |
| else |
| { |
| modifyRect.height -= lastRect.height; |
| |
| //if this is RTL, we need to shift the selection block down by the amount |
| //we reduced it. |
| if(direction == Direction.RTL) |
| modifyRect.top += lastRect.height; |
| } |
| } |
| } |
| } |
| } |
| |
| normalizeRects(blockRectArray, rectArray, largestRise, blockProgression, direction); |
| //add in the TCY Rects |
| if(tcyDrawRects && tcyDrawRects.length > 0) |
| { |
| for(var tcyIter:int = 0; tcyIter < tcyDrawRects.length; ++tcyIter) |
| { |
| rectArray.push(tcyDrawRects[tcyIter]); |
| } |
| } |
| |
| //float selections do not normalize, put them into the rect array now |
| if(floatRectArray) |
| { |
| for(var floatIter:int = 0; floatIter < floatRectArray.length; ++floatIter) |
| { |
| rectArray.push(floatRectArray[floatIter]); |
| } |
| } |
| |
| } |
| |
| private function createSelectionShapes(selObj:Shape, selFormat:SelectionFormat, container:DisplayObject, begIdx:int, endIdx:int, prevLine:TextFlowLine, nextLine:TextFlowLine):void |
| { |
| var contElement:ContainerFormattedElement = _para.getAncestorWithContainer(); |
| CONFIG::debug { assert(contElement != null,"para with no container"); } |
| var blockProgression:String = contElement.computedFormat.blockProgression; |
| |
| var selCache:SelectionCache = getSelectionShapesCacheEntry(begIdx, endIdx, prevLine, nextLine, blockProgression); |
| if (!selCache) |
| return; |
| |
| //iterate the blocks and create DisplayObjects to draw... |
| var drawRect:Rectangle; |
| var color:uint = selFormat.rangeColor; |
| |
| if (_para && _para.getTextFlow()) { |
| var selMgr:ISelectionManager = _para.getTextFlow().interactionManager; |
| if (selMgr && (selMgr.anchorPosition == selMgr.activePosition)) |
| color = selFormat.pointColor; |
| } |
| |
| for each (drawRect in selCache.selectionBlocks) |
| { |
| drawRect = drawRect.clone(); |
| convertLineRectToContainer(drawRect, true); |
| createSelectionRect(selObj, color, drawRect.x, drawRect.y, drawRect.width, drawRect.height); |
| } |
| } |
| |
| /** @private |
| * Get the height and vertical adjustment for the line's selection shape, assuming Western typographic rules |
| * where leading is included in selection. |
| * @return An array with two elements |
| * [0] height |
| * [1] vertical adjustment to counter 'align bottom' behavior. The remainder of the selection code assumes selection shape |
| * bottom is to be aligned with line descent. If this is not the case, vertical adjustment is set to an appropriate non-zero value. |
| */ |
| tlf_internal function getRomanSelectionHeightAndVerticalAdjustment (prevLine:TextFlowLine, nextLine:TextFlowLine):Array |
| { |
| var rectHeight:Number = 0; |
| var verticalAdj:Number = 0; // Default to 'align bottom'. |
| |
| //This code erroneously assumed that it would only be called with a SPACE justifier and that AUTO would be up. That is incorrect |
| //because some scripts, like Korean, use an up leading model and the EAST_ASIAN justifier. New code just performs the check |
| if(ParagraphElement.useUpLeadingDirection(_para.getEffectiveLeadingModel())) |
| { |
| // "Space above, align bottom" |
| // 1) Space above as dictated by first baseline offset for the first line or line leading otherwise (both obtained from the 'height' data member) |
| // 2) Selection rectangle must at least include all of the text area |
| rectHeight = height > _textHeight ? height : _textHeight; |
| |
| // 3) Selection rectangle's bottom aligned with line descent; verticalAdj remains 0 |
| } |
| else |
| { |
| // TODO-9/4/08-Is this the right way to check for first/last lines? |
| var isFirstLine:Boolean = !prevLine || prevLine.controller != controller || prevLine.columnIndex != columnIndex; |
| var isLastLine:Boolean = !nextLine || nextLine.controller != controller || nextLine.columnIndex != columnIndex |
| || nextLine.paragraph.getEffectiveLeadingModel() == LeadingModel.ROMAN_UP; |
| //I'm removing this line as it makes the assumption that AUTO leading dir is UP only for Roman text, which is incorrect. |
| //Korean also uses UP leading but uses the EastAsian justifier. - gak 01.22.09 |
| //||(nextLine.paragraph.computedFormat.leadingDirection == LeadingDirection.AUTO && nextLine.paragraph.computedFormat.justificationRule == JustificationRule.SPACE); |
| |
| if (isLastLine) |
| { |
| // There is no line after this one, or there is one which uses leading UP, so leading DOWN does not apply |
| |
| if (!isFirstLine) |
| { |
| // "Space above None, align bottom" (eqivalently, "Space below None, align top"): |
| // 1) Only the text area should be selected |
| rectHeight = _textHeight; |
| |
| // 2) Selection rectangle's bottom aligned with line descent; verticalAdj remains 0 |
| } |
| else |
| { |
| // "Space above, align bottom" |
| // 1) Space above as dictated by first baseline offset |
| // 2) Selection rectangle must at least include all of the text area |
| rectHeight = height > _textHeight ? height : _textHeight; |
| // 3) Selection rectangle's bottom aligned with line descent; verticalAdj remains 0 |
| } |
| } |
| else |
| { |
| // There is a line after this one, so leading DOWN applies |
| |
| if (!isFirstLine) |
| { |
| // "Space below, align top" |
| // 1) Space below as dictated by line leading (obtained from 'height' member of next line) |
| // 2) Selection rectangle must at least include all of the text area |
| rectHeight = nextLine.height > _textHeight ? nextLine.height : _textHeight; |
| |
| // 3) Selection rectangle's top to be aligned with line ascent, so its bottom to be at rectHeight - textLine.ascent, |
| // not textLine.descent, set verticalAdj accordingly |
| verticalAdj = rectHeight - _textHeight; // same as rectHeight - textLine.ascent - textLine.descent |
| } |
| else |
| { |
| // Union of |
| // 1) first line, leading up: In this case, rectangle height is the larger of line height and text height, |
| // and the rectangle is shifted down by descent amount to align bottoms. So, top of rectangle is at: |
| var top:Number = _descent - (height > _textHeight ? height : _textHeight); |
| |
| // 2) interior line, leading down: In this case, rectangle height is the larger of line leading and text height, |
| // and the rectangle is shifted up by ascent amount to align tops. So, bottom of rectangle is at: |
| var bottom:Number = (nextLine.height > _textHeight ? nextLine.height : _textHeight) - _ascent; |
| |
| rectHeight = bottom - top; |
| |
| // 3) Selection rectangle's bottom to be at 'bottom', not the line's descent; set verticalAdj accordingly |
| verticalAdj = bottom - _descent; |
| } |
| } |
| } |
| |
| //If we don't have a line above us, then we need to pad the line a bit as well as make it shift up. |
| //If we don't, then it overlaps the line below too much OR clips the top of the glyphs. |
| if(!prevLine || prevLine.columnIndex != this.columnIndex || prevLine.controller != this.controller) |
| { |
| //make it taller - this is kinda a fudge, but we have no info to determine a good top. |
| //if we don't do this, the selection rectangle will clip to the top of the glyphs and even |
| //let parts stick out a bit. So, re-add the descent and offset the rect by 50% so that |
| //it appears to balance the top and bottom. |
| rectHeight += this.descent; |
| verticalAdj = Math.floor(this.descent/2); |
| } |
| return [rectHeight, verticalAdj]; |
| } |
| |
| /** @private |
| * |
| * |
| */ |
| private function makeSelectionBlocks(begIdx:int, endIdx:int, paraAbsStart:int, blockProgression:String, direction:String, heightAndAdj:Array):Array |
| { |
| CONFIG::debug{ assert(begIdx <= endIdx, "Selection indexes are reversed! How can this happen?!"); } |
| |
| var blockArray:Array = new Array(); |
| var blockRect:Rectangle = new Rectangle(); |
| var startElem:FlowLeafElement = _para.findLeaf(begIdx); |
| var startMetrics:Rectangle = startElem.getComputedFontMetrics().emBox; |
| |
| var textLine:TextLine = getTextLine(true); |
| |
| //++makeBlockPassCounter; |
| //trace(makeBlockPassCounter + ") direction = " + direction + " blockProgression = " + blockProgression); |
| |
| //if this is the whole line, then we should use line data to perform the selection |
| if(paraAbsStart + begIdx == absoluteStart && paraAbsStart + endIdx == absoluteStart + textLength) |
| { |
| var globalStart:Point = new Point(0,0); |
| var justRule:String = _para.getEffectiveJustificationRule(); |
| |
| //use the textLine info if we're not using J justification |
| if(justRule != JustificationRule.EAST_ASIAN) |
| { |
| if(blockProgression == BlockProgression.RL) |
| { |
| globalStart.x -= heightAndAdj[1]; |
| blockRect.width = heightAndAdj[0]; |
| blockRect.height = textLine.textWidth == 0 ? EMPTY_LINE_WIDTH : textLine.textWidth; |
| } |
| else |
| { |
| globalStart.y += heightAndAdj[1]; |
| blockRect.height = heightAndAdj[0]; |
| blockRect.width = textLine.textWidth == 0 ? EMPTY_LINE_WIDTH : textLine.textWidth; |
| } |
| |
| } |
| else |
| { |
| var eaStartElem:int = textLine.getAtomIndexAtCharIndex(begIdx); |
| var eaStartRect:Rectangle = textLine.getAtomBounds(eaStartElem); |
| |
| if(blockProgression == BlockProgression.RL) |
| { |
| blockRect.width = eaStartRect.width; |
| blockRect.height = textLine.textWidth; |
| } |
| else |
| { |
| blockRect.height = eaStartRect.height; |
| blockRect.width = textLine.textWidth; |
| } |
| } |
| |
| blockRect.x = globalStart.x; |
| blockRect.y = globalStart.y; |
| |
| if(blockProgression == BlockProgression.RL) |
| { |
| blockRect.x -= textLine.descent; |
| } |
| else |
| { |
| blockRect.y += (textLine.descent - blockRect.height) |
| } |
| |
| //handle rotation |
| if(startElem.computedFormat.textRotation == TextRotation.ROTATE_180 || |
| startElem.computedFormat.textRotation == TextRotation.ROTATE_90) |
| { |
| if(blockProgression != BlockProgression.RL) |
| blockRect.y += blockRect.height / 2; |
| else |
| blockRect.x -= blockRect.width; |
| } |
| //push it onto array |
| blockArray.push(blockRect); |
| } |
| else //we only have part of the line. Get the start and end TC bounds |
| { |
| //trace(makeBlockPassCounter + ") begIdx = " + begIdx.toString() + " endIdx = " + endIdx.toString()); |
| var begElementIndex:int = textLine.getAtomIndexAtCharIndex(begIdx); |
| var endElementIndex:int = adjustEndElementForBidi(begIdx, endIdx, begElementIndex, direction); |
| |
| //trace(makeBlockPassCounter + ") begElementIndex = " + begElementIndex.toString() + " endElementIndex = " + endElementIndex.toString()); |
| CONFIG::debug{ assert(begElementIndex >= 0, "Invalid start index! begIdx = " + begIdx)}; |
| CONFIG::debug{ assert(endElementIndex >= 0, "Invalid end index! begIdx = " + endIdx)}; |
| |
| if(direction == Direction.RTL && textLine.getAtomBidiLevel(endElementIndex)%2 != 0) |
| { |
| //if we are in RTL, anchoring the LTR text gets tricky. Because the endElement is before the first |
| //element - which is why we're in this code - the result can be a zero-width rectangle if the span of LTR |
| //text breaks across line boundaries. If that is the case, then the endElementIndex value will be 0. As |
| //this is the less common case, assume that it isn't and make all other cases come first |
| if (endElementIndex == 0 && begIdx < endIdx-1) |
| { |
| //since the endElementIndex is 0, meaning that the LTR spans lines, |
| //we want to grab the glyph before the endIdx which represents the last LTR glyph for the selection. |
| //Make a recursive call into makeSelectionBlocks using and endIdx decremented by 1. |
| blockArray = makeSelectionBlocks(begIdx, endIdx - 1, paraAbsStart, blockProgression, direction, heightAndAdj); |
| var bidiBlock:Array = makeSelectionBlocks(endIdx - 1, endIdx - 1, paraAbsStart, blockProgression, direction, heightAndAdj) |
| var bidiBlockIter:int = 0; |
| while(bidiBlockIter < bidiBlock.length) |
| { |
| blockArray.push(bidiBlock[bidiBlockIter]); |
| ++bidiBlockIter; |
| } |
| return blockArray; |
| } |
| } |
| |
| var begIsBidi:Boolean = begElementIndex != -1 ? isAtomBidi(begElementIndex, direction) : false; |
| var endIsBidi:Boolean = endElementIndex != -1 ? isAtomBidi(endElementIndex, direction) : false; |
| |
| //trace("begElementIndex is bidi = " + begIsBidi.toString()); |
| //trace("endElementIndex is bidi = " + endIsBidi.toString()); |
| |
| if(begIsBidi || endIsBidi) |
| { |
| //this code needs to iterate over the glyphs starting at the begElementIndex and going forward. |
| //It doesn't matter is beg is bidi or not, we need to find a boundary, create a rect on it, then proceded. |
| //use the value of begIsBidi for testing the consistency of the selection. |
| |
| //Example bidi text. Note that the period comes at the left end of the line: |
| // |
| // Bidi state: f f t t t t t (true/false) |
| // Element Index:0 1 2 3 4 5 6 (0 is the para terminator) |
| // Chars: . t o _ b e |
| // Flow Index: 6 0 1 2 3 4 (5) Note that these numbers represent the space between glyphs AND |
| // 5(f) that index 5 is both the space after the e and before the period. |
| // but, the position 5 is not a valid cursor location. |
| |
| //the original code I implemented used the beg and endElement indexes however that fails because when the text |
| //is mixed bidi/non-bidi, the indexes are only 1 char apart. This resulted in, for example, only the period in |
| //a line getting selected when the text was bidi. Instead, we're going to use the begIdx and endIdx and |
| //recalculate the element indexes each time. This is expensive, but I don't see an alternative. - gak 09.05.08 |
| var curIdx:int = begIdx; |
| var incrementor:int = (begIdx != endIdx ? 1 : 0); |
| |
| //the indexes used to draw the seleciton. activeStart/End represent the |
| //beginning of the selection shape atoms, while cur is the one we are testing. |
| var activeStartIndex:int = begElementIndex; |
| var activeEndIndex:int = begElementIndex; |
| var curElementIndex:int = begElementIndex; |
| |
| //when activeEndIsBidi no longer matches the bidi setting for the activeStartIndex, we will create the shape |
| var activeEndIsBidi:Boolean = begIsBidi; |
| |
| do |
| { |
| //increment the index |
| curIdx += incrementor; |
| //get the next atom index |
| curElementIndex = textLine.getAtomIndexAtCharIndex(curIdx); |
| |
| //calculate the bidi level for the - kinda cludgy, but if the bidi-text wraps, curElementIndex == -1 |
| //so just set it to false if this is the case. It will get ignored in the subsequent check and curIdx |
| //will == endIdx as this is the last glyph in the line - which is why the next is -1 - gak 09.12.08 |
| var curIsBidi:Boolean = (curElementIndex != -1) ? isAtomBidi(curElementIndex, direction) : false; |
| |
| if(curElementIndex != -1 && curIsBidi != activeEndIsBidi) |
| { |
| blockRect = makeBlock(activeStartIndex, activeEndIndex, startMetrics, blockProgression, direction, heightAndAdj); |
| blockArray.push(blockRect); |
| |
| //shift the activeStart/End indexes to the current |
| activeStartIndex = curElementIndex; |
| activeEndIndex = curElementIndex; |
| //update the bidi setting |
| activeEndIsBidi = curIsBidi; |
| } |
| else |
| { |
| //we don't get another chance to make a block, so if this is the last char, make the block before we bail out. |
| //we have to check both equality and equality plus the incrementor because if we don't, then we'll miss a |
| //character in the selection. |
| if(curIdx == endIdx) |
| { |
| blockRect = makeBlock(activeStartIndex, activeEndIndex, startMetrics, blockProgression, direction, heightAndAdj); |
| blockArray.push(blockRect); |
| } |
| |
| activeEndIndex = curElementIndex; |
| } |
| }while(curIdx < endIdx) |
| |
| } |
| else |
| { |
| var testILG:InlineGraphicElement = startElem as InlineGraphicElement; |
| if(!testILG || testILG.float == Float.NONE) |
| { |
| blockRect = makeBlock(begElementIndex, endElementIndex, startMetrics, blockProgression, direction, heightAndAdj); |
| } |
| else |
| { |
| blockRect = testILG.graphic.getBounds(textLine); |
| } |
| |
| blockArray.push(blockRect); |
| } |
| |
| } |
| |
| return blockArray; |
| } |
| |
| /** @private |
| * |
| * |
| */ |
| private function makeBlock(begElementIndex:int, endElementIndex:int, startMetrics:Rectangle, blockProgression:String, direction:String, heightAndAdj:Array):Rectangle |
| { |
| var blockRect:Rectangle = new Rectangle(); |
| var globalStart:Point = new Point(0,0); |
| var heightAndAdj:Array; |
| |
| if(begElementIndex > endElementIndex) |
| { |
| // swap the start and end |
| var tempEndIdx:int = endElementIndex; |
| endElementIndex = begElementIndex; |
| begElementIndex = tempEndIdx; |
| } |
| var textLine:TextLine = getTextLine(true); |
| |
| //now that we have elements and they are in the right order for drawing, get their rectangles |
| var begCharRect:Rectangle = textLine.getAtomBounds(begElementIndex); |
| //trace(makeBlockPassCounter + ") begCharRect = " + begCharRect.toString()); |
| |
| var endCharRect:Rectangle = textLine.getAtomBounds(endElementIndex); |
| //trace(makeBlockPassCounter + ") endCharRect = " + endCharRect.toString()); |
| |
| //Calculate the justificationRule value |
| var justRule:String = _para.getEffectiveJustificationRule(); |
| //If this is TTB text and NOT TCY, as indicated by TextRotation.rotate0... |
| if(blockProgression == BlockProgression.RL && textLine.getAtomTextRotation(begElementIndex) != TextRotation.ROTATE_0) |
| { |
| globalStart.y = begCharRect.y; |
| blockRect.height = begElementIndex != endElementIndex ? endCharRect.bottom - begCharRect.top : begCharRect.height; |
| |
| //re-ordered this code. EAST_ASIAN is more common in vertical and should be the first option. |
| if(justRule == JustificationRule.EAST_ASIAN) |
| { |
| blockRect.width = begCharRect.width; |
| } |
| else |
| { |
| blockRect.width = heightAndAdj[0]; |
| globalStart.x -= heightAndAdj[1]; |
| } |
| } |
| else |
| { |
| //given bidi text alternations, the endCharRect could be left of the begCharRect, |
| //use whichever is farther left. |
| globalStart.x = (begCharRect.x < endCharRect.x ? begCharRect.x : endCharRect.x); |
| //if we're here and the BlockProgression is TTB, then we're TCY. Less frequent case, so make non-TCY |
| //the first option... |
| //NB - Never use baseline adjustments for TCY. They don't make sense here.(I think) - gak 06.03.08 |
| if(blockProgression == BlockProgression.RL) |
| globalStart.y = begCharRect.y + (startMetrics.width /2); // TODO-9/5/8:Behavior for leading down TBD |
| |
| |
| if(justRule != JustificationRule.EAST_ASIAN) |
| { |
| blockRect.height = heightAndAdj[0]; |
| if(blockProgression == BlockProgression.RL) |
| globalStart.x -= heightAndAdj[1]; |
| else |
| globalStart.y += heightAndAdj[1]; |
| //changed the width from a default of 2 to use the begCharRect.width so that point seletion |
| //can choose to use the right or left side of the glyph when drawing a caret Watson 1876415/1876953- gak 08.19.09 |
| blockRect.width = begElementIndex != endElementIndex ? Math.abs(endCharRect.right - begCharRect.left) : begCharRect.width; |
| } |
| else |
| { |
| blockRect.height = begCharRect.height; |
| |
| //changed the width from a default of 2 to use the begCharRect.width so that point seletion |
| //can choose to use the right or left side of the glyph when drawing a caret Watson 1876415/1876953- gak 08.19.09 |
| blockRect.width = begElementIndex != endElementIndex ? Math.abs(endCharRect.right - begCharRect.left) : begCharRect.width; |
| } |
| } |
| |
| blockRect.x = globalStart.x; |
| blockRect.y = globalStart.y; |
| if(blockProgression == BlockProgression.RL) |
| { |
| if(textLine.getAtomTextRotation(begElementIndex) != TextRotation.ROTATE_0) |
| blockRect.x -= textLine.descent; |
| else //it's TCY |
| blockRect.y -= (blockRect.height / 2) |
| } |
| else |
| { |
| blockRect.y += (textLine.descent - blockRect.height); |
| } |
| |
| var tfl:TextFlowLine = textLine.userData as TextFlowLine; |
| var curElem:FlowLeafElement = _para.findLeaf(textLine.textBlockBeginIndex + begElementIndex); |
| var rotation:String = curElem.computedFormat.textRotation; |
| |
| //handle rotation. For horizontal text, rotations of 90 or 180 cause the text |
| //to draw under the baseline in a cosistent location. Vertical text is a bit more complicated |
| //in that a 90 rotation puts it immediately to the left of the Em Box, whereas 180 is one quarter |
| //of the way in the Em Box. Fix for Watson 1915930 - gak 02.17.09 |
| if(rotation == TextRotation.ROTATE_180 || |
| rotation == TextRotation.ROTATE_90) |
| { |
| if(blockProgression != BlockProgression.RL) |
| blockRect.y += (blockRect.height / 2); |
| else |
| { |
| if(curElem.getParentByType(TCYElement) == null) |
| { |
| if(rotation == TextRotation.ROTATE_90) |
| blockRect.x -= blockRect.width; |
| else |
| blockRect.x -= (blockRect.width * .75); |
| } |
| else |
| { |
| if(rotation == TextRotation.ROTATE_90) |
| blockRect.y += blockRect.height; |
| else |
| blockRect.y += (blockRect.height * .75); |
| } |
| } |
| } |
| |
| |
| return blockRect; |
| } |
| |
| /** @private |
| * |
| * |
| */ |
| tlf_internal function convertLineRectToContainer(rect:Rectangle, constrainShape:Boolean):void |
| { |
| var textLine:TextLine = getTextLine(); |
| |
| /* var globalStart:Point = new Point(rect.x, rect.y); |
| |
| //convert to controller coordinates... |
| ////trace(makeBlockPassCounter + ") globalStart = " + globalStart.toString()); |
| globalStart = textLine.localToGlobal(globalStart); |
| ////trace(makeBlockPassCounter + ") localToGlobal.globalStart = " + globalStart.toString()); |
| globalStart = container.globalToLocal(globalStart); |
| ////trace(makeBlockPassCounter + ") globalToLocal.globalStart = " + globalStart.toString()); |
| rect.x = globalStart.x; |
| rect.y = globalStart.y; */ |
| |
| // this is much simpler and actually more accurate - localToGlobal/globalToLocal does some rounding |
| rect.x += textLine.x; |
| rect.y += textLine.y; |
| |
| if (constrainShape) |
| { |
| var tf:TextFlow = _para.getTextFlow(); |
| var columnRect:Rectangle = controller.columnState.getColumnAt(this.columnIndex); |
| constrainRectToColumn(tf,rect,columnRect,controller.horizontalScrollPosition,controller.verticalScrollPosition,controller.compositionWidth,controller.compositionHeight); |
| } |
| } |
| |
| /** @private */ |
| static tlf_internal function constrainRectToColumn(tf:TextFlow,rect:Rectangle,columnRect:Rectangle,hScrollPos:Number,vScrollPos:Number,compositionWidth:Number,compositionHeight:Number):void |
| { |
| if(tf.computedFormat.lineBreak == LineBreak.EXPLICIT) |
| return; |
| |
| var bp:String = tf.computedFormat.blockProgression; |
| var direction:String = tf.computedFormat.direction; |
| |
| if(bp == BlockProgression.TB && !isNaN(compositionWidth)) |
| { |
| if(direction == Direction.LTR) |
| { |
| //make sure is doesn't go past the end of the container |
| if(rect.left > (columnRect.x + columnRect.width + hScrollPos)) |
| rect.left = (columnRect.x + columnRect.width + hScrollPos); |
| |
| //make sure that if this is a selection and not a point selection, that |
| //we don't go beyond the end of the container... |
| if(rect.right > (columnRect.x + columnRect.width + hScrollPos)) |
| rect.right = (columnRect.x + columnRect.width + hScrollPos); |
| } |
| else |
| { |
| if(rect.right < (columnRect.x + hScrollPos)) |
| rect.right = (columnRect.x + hScrollPos); |
| |
| if(rect.left < (columnRect.x + hScrollPos)) |
| rect.left = (columnRect.x + hScrollPos); |
| } |
| } |
| else if (bp == BlockProgression.RL && !isNaN(compositionHeight)) |
| { |
| if(direction == Direction.LTR) |
| { |
| //make sure is doesn't go past the end of the container |
| if(rect.top > (columnRect.y + columnRect.height + vScrollPos)) |
| rect.top = (columnRect.y + columnRect.height + vScrollPos); |
| |
| //make sure that if this is a selection and not a point selection, that |
| //we don't go beyond the end of the container... |
| if(rect.bottom > (columnRect.y + columnRect.height + vScrollPos)) |
| rect.bottom = (columnRect.y + columnRect.height + vScrollPos); |
| } |
| else |
| { |
| if(rect.bottom < (columnRect.y + vScrollPos)) |
| rect.bottom = (columnRect.y + vScrollPos); |
| |
| if(rect.top < (columnRect.y + vScrollPos)) |
| rect.top = (columnRect.y + vScrollPos); |
| } |
| } |
| } |
| |
| /** @private |
| * Helper method to hilight the portion of a block selection on this TextLine. A selection display is created and added to the line's TextFrame with ContainerController addSelectionShape. |
| * @param begIdx absolute index of start of selection on this line. |
| * @param endIdx absolute index of end of selection on this line. |
| */ |
| tlf_internal function hiliteBlockSelection(selObj:Shape, selFormat:SelectionFormat, container:DisplayObject, begIdx:int,endIdx:int, prevLine:TextFlowLine, nextLine:TextFlowLine):void |
| { |
| // no container for overflow lines, or lines scrolled out |
| if (isDamaged() || !_controller) |
| return; |
| |
| // CONFIG::debug { assert(_textLineCache != null, "bad call to hiliteBlockSelection"); } |
| |
| var textLine:TextLine = peekTextLine(); |
| if (!textLine || !textLine.parent) |
| return; |
| |
| var paraStart:int = _para.getAbsoluteStart(); |
| begIdx -= paraStart; |
| endIdx -= paraStart; |
| |
| createSelectionShapes(selObj, selFormat, container, begIdx, endIdx, prevLine, nextLine); |
| } |
| |
| /** @private |
| * Helper method to hilight a point selection on this TextLine. x,y,w,h of the selection are calculated and ContainerController.drawPointSelection is called |
| * @param idx absolute index of the point selection. |
| */ |
| tlf_internal function hilitePointSelection(selFormat:SelectionFormat, idx:int, container:DisplayObject, prevLine:TextFlowLine, nextLine:TextFlowLine):void |
| { |
| var rect:Rectangle = computePointSelectionRectangle(idx,container,prevLine,nextLine, true); |
| if (rect) |
| _controller.drawPointSelection(selFormat, rect.x, rect.y, rect.width, rect.height) |
| } |
| |
| static private function setRectangleValues(rect:Rectangle,x:Number,y:Number,width:Number,height:Number):void |
| { |
| rect.x = x; |
| rect.y = y; |
| rect.width = width; |
| rect.height = height; |
| } |
| |
| /** @private */ |
| tlf_internal function computePointSelectionRectangle(idx:int, container:DisplayObject, prevLine:TextFlowLine, nextLine:TextFlowLine, constrainSelRect:Boolean):Rectangle |
| { |
| if (isDamaged() || !_controller) |
| return null; |
| |
| // CONFIG::debug { assert(_textLineCache != null, "bad call to hiliteBlockSelection"); } |
| |
| // no container for overflow lines, or lines scrolled out |
| var textLine:TextLine = peekTextLine(); |
| if (!textLine || !textLine.parent) |
| return null; |
| // adjust to this paragraph's TextBlock |
| idx -= _para.getAbsoluteStart(); |
| |
| textLine = getTextLine(true); |
| |
| //endIdx will only differ if idx is altered when detecting TCY bounds |
| var endIdx:int = idx; |
| var elementIndex:int = textLine.getAtomIndexAtCharIndex(idx); |
| CONFIG::debug{ assert(elementIndex != -1, "Invalid point selection index! idx = " + idx); } |
| |
| var isTCYBounds:Boolean = false; |
| var paraLeadingTCY:Boolean = false; |
| |
| var contElement:ContainerFormattedElement = _para.getAncestorWithContainer(); |
| CONFIG::debug { assert(contElement != null,"para with no container"); } |
| var blockProgression:String = contElement.computedFormat.blockProgression; |
| var direction:String = _para.computedFormat.direction; |
| |
| //need to check for TCY. TCY cannot take input into it's head, but can in it's tail. |
| if(blockProgression == BlockProgression.RL) |
| { |
| if (idx == 0) |
| { |
| if(textLine.getAtomTextRotation(0) == TextRotation.ROTATE_0) |
| paraLeadingTCY = true; |
| } |
| else |
| { |
| var prevElementIndex:int = textLine.getAtomIndexAtCharIndex(idx - 1); |
| if(prevElementIndex != -1) |
| { |
| //if this elem is TCY, then we need to back up one space and use the right bounds |
| if(textLine.getAtomTextRotation(elementIndex) == TextRotation.ROTATE_0 && |
| textLine.getAtomTextRotation(prevElementIndex) != TextRotation.ROTATE_0) |
| { |
| elementIndex = prevElementIndex; |
| --idx; |
| isTCYBounds = true; |
| } |
| else if(textLine.getAtomTextRotation(prevElementIndex) == TextRotation.ROTATE_0) |
| { |
| elementIndex = prevElementIndex; |
| --idx; |
| isTCYBounds = true; |
| } |
| } |
| } |
| } |
| |
| var heightAndAdj:Array = getRomanSelectionHeightAndVerticalAdjustment(prevLine, nextLine); |
| var blockRectArray:Array = makeSelectionBlocks(idx, endIdx, _para.getAbsoluteStart(), blockProgression, direction, heightAndAdj); |
| CONFIG::debug{ assert(blockRectArray.length == 1, "A point selection should return a single selection rectangle!"); } |
| var rect:Rectangle = blockRectArray[0]; |
| |
| convertLineRectToContainer(rect, constrainSelRect); |
| |
| var drawOnRight:Boolean = (direction == Direction.RTL); |
| |
| if((drawOnRight && textLine.getAtomBidiLevel(elementIndex) % 2 == 0) || |
| (!drawOnRight && textLine.getAtomBidiLevel(elementIndex) % 2 != 0)) |
| { |
| drawOnRight = !drawOnRight; |
| } |
| |
| if(blockProgression == BlockProgression.RL && textLine.getAtomTextRotation(elementIndex) != TextRotation.ROTATE_0) |
| { |
| if(!drawOnRight) |
| setRectangleValues(rect, rect.x, !isTCYBounds ? rect.y : rect.y + rect.height,rect.width,1); |
| else |
| setRectangleValues(rect, rect.x, !isTCYBounds ? rect.y + rect.height : rect.y ,rect.width,1); |
| } |
| else |
| { |
| //choose to use the right or left side of the glyph based on Direction when drawing a caret Watson 1876415/1876953 |
| //if the direction is ltr, then the cursor should be on the left side |
| if(!drawOnRight) |
| setRectangleValues(rect, !isTCYBounds ? rect.x : rect.x + rect.width, rect.y, 1, rect.height); |
| else //otherwise, it should be on the right, unless it is TCY |
| setRectangleValues(rect, !isTCYBounds ? rect.x + rect.width : rect.x, rect.y, 1, rect.height); |
| } |
| |
| //allow the atoms to be garbage collected. |
| textLine.flushAtomData(); |
| |
| return rect; |
| } |
| |
| /** @private |
| * Three states. Disjoint(0), Intersects(1), HeightContainedIn(2), |
| */ |
| |
| tlf_internal function selectionWillIntersectScrollRect(scrollRect:Rectangle, begIdx:int, endIdx:int, prevLine:TextFlowLine, nextLine:TextFlowLine):int |
| { |
| var contElement:ContainerFormattedElement = _para.getAncestorWithContainer(); |
| CONFIG::debug { assert(contElement != null,"para with no container"); } |
| var blockProgression:String = contElement.computedFormat.blockProgression; |
| var textLine:TextLine = getTextLine(true); |
| |
| if (begIdx == endIdx) |
| { |
| var pointSelRect:Rectangle = computePointSelectionRectangle(begIdx, DisplayObject(controller.container), prevLine, nextLine, false); |
| if (pointSelRect) |
| { |
| if (scrollRect.containsRect(pointSelRect)) |
| return 2; |
| if (scrollRect.intersects(pointSelRect)) |
| return 1; |
| } |
| } |
| else |
| { |
| var paraStart:int = _para.getAbsoluteStart(); |
| var selCache:SelectionCache = this.getSelectionShapesCacheEntry(begIdx-paraStart,endIdx-paraStart,prevLine,nextLine,blockProgression); |
| if (selCache) |
| { |
| //iterate the blocks and check for intersections |
| var drawRect:Rectangle; |
| for each (drawRect in selCache.selectionBlocks) |
| { |
| drawRect = drawRect.clone(); |
| // convertLineRectToContainer(container, drawRect); |
| drawRect.x += textLine.x; |
| drawRect.y += textLine.y; |
| if (scrollRect.intersects(drawRect)) |
| { |
| if(blockProgression == BlockProgression.RL) |
| { |
| // see if width is entirely contained in scrollRect |
| if (drawRect.left >= scrollRect.left && drawRect.right <= scrollRect.right) |
| return 2; |
| } |
| else |
| { |
| if (drawRect.top >= scrollRect.top && drawRect.bottom <= scrollRect.bottom) |
| return 2; |
| } |
| return 1; |
| } |
| } |
| } |
| } |
| return 0; |
| } |
| |
| |
| /** @private */ |
| CONFIG::debug private static function dumpAttribute(result:String, attributeName:String, attributeValue:Object):String |
| { |
| if (attributeValue) |
| { |
| result += " "; |
| result += attributeName; |
| result += "=\""; |
| result += attributeValue.toString(); |
| result += "\"" |
| } |
| return result; |
| } |
| |
| /** @private |
| */ |
| private function normalizeRects(srcRects:Array, dstRects:Array, largestRise:Number, blockProgression:String, direction:String):void |
| { |
| //the last rectangle in the list with a potential to merge |
| var lastRect:Rectangle = null; |
| var rectIter:int = 0; |
| while(rectIter < srcRects.length) |
| { |
| //get the current rect and advance the iterator |
| var rect:Rectangle = srcRects[rectIter++]; |
| |
| //apply a new height if needed. |
| if(blockProgression == BlockProgression.RL) |
| { |
| if(rect.width < largestRise) |
| { |
| rect.width = largestRise; |
| } |
| } |
| else |
| { |
| if(rect.height < largestRise) |
| { |
| rect.height = largestRise; |
| } |
| } |
| //if the lastRect is null, no need to perform calculation |
| if(lastRect == null) |
| { |
| lastRect = rect; |
| } |
| else |
| { |
| //TCY has already been excluded, so no need to worry about it here... |
| if(blockProgression == BlockProgression.RL) |
| { |
| //trace(normalCounter + ") lastRect = " + lastRect.toString()); |
| //trace(normalCounter + ") rect = " + rect.toString()); |
| |
| //merge it in to the last rect |
| if(lastRect.y < rect.y && (lastRect.y + lastRect.height) >= rect.top && lastRect.x == rect.x) |
| { |
| lastRect.height += rect.height; |
| } |
| else if(rect.y < lastRect.y && lastRect.y <= rect.bottom && lastRect.x == rect.x) |
| { |
| lastRect.height += rect.height; |
| lastRect.y = rect.y; |
| } |
| else |
| { |
| //we have a break in the rectangles and should push last rect onto the draw list before continuing |
| dstRects.push(lastRect); |
| lastRect = rect; |
| } |
| } |
| else |
| { |
| if(lastRect.x < rect.x && (lastRect.x + lastRect.width) >= rect.left && lastRect.y == rect.y) |
| { |
| lastRect.width += rect.width; |
| } |
| else if(rect.x < lastRect.x && lastRect.x <= rect.right && lastRect.y == rect.y) |
| { |
| lastRect.width += rect.width; |
| lastRect.x = rect.x; |
| } |
| else |
| { |
| //we have a break in the rectangles and should push last rect onto the draw list before continuing |
| dstRects.push(lastRect); |
| lastRect = rect; |
| } |
| } |
| } |
| //if this is the last rectangle, we haven't added it, do so now. |
| if(rectIter == srcRects.length) |
| dstRects.push(lastRect); |
| } |
| } |
| |
| /** @private */ |
| private function adjustEndElementForBidi(begIdx:int, endIdx:int, begElementIndex:int, direction:String):int |
| { |
| var endElementIndex:int = begElementIndex; |
| |
| var textLine:TextLine = getTextLine(true); |
| |
| if(endIdx != begIdx) |
| { |
| if(((direction == Direction.LTR && textLine.getAtomBidiLevel(begElementIndex)%2 != 0) |
| || (direction == Direction.RTL && textLine.getAtomBidiLevel(begElementIndex)%2 == 0)) |
| && textLine.getAtomTextRotation(begElementIndex) != TextRotation.ROTATE_0) |
| endElementIndex = textLine.getAtomIndexAtCharIndex(endIdx); |
| else |
| { |
| endElementIndex = textLine.getAtomIndexAtCharIndex(endIdx - 1); |
| } |
| } |
| |
| if(endElementIndex == -1 && endIdx > 0) |
| { |
| return adjustEndElementForBidi(begIdx, endIdx - 1, begElementIndex, direction); |
| } |
| return endElementIndex; |
| } |
| |
| /** @private */ |
| private function isAtomBidi(elementIdx:int, direction:String):Boolean |
| { |
| var textLine:TextLine = getTextLine(true); |
| |
| return (textLine.getAtomBidiLevel(elementIdx)%2 != 0 && direction == Direction.LTR) || (textLine.getAtomBidiLevel(elementIdx)%2 == 0 && direction == Direction.RTL); |
| } |
| |
| /** @private */ |
| tlf_internal function get adornCount():int |
| { return _adornCount; } |
| |
| /** @private */ |
| CONFIG::debug public function dumpToXML():String |
| { |
| var result:String = new String("<line"); |
| |
| result = dumpAttribute(result, "absoluteStart", absoluteStart); |
| result = dumpAttribute(result, "textLength", textLength); |
| result = dumpAttribute(result, "height", height); |
| result = dumpAttribute(result, "spaceBefore", spaceBefore); |
| |
| result = dumpAttribute(result, "spaceAfter", spaceAfter); |
| result = dumpAttribute(result, "location", location); |
| result = dumpAttribute(result, "x", x); |
| result = dumpAttribute(result, "y", y); |
| result = dumpAttribute(result, "targetWidth", targetWidth); |
| result = dumpAttribute(result, "lineOffset", _lineOffset); |
| result += ">\n"; |
| |
| |
| var textLine:TextLine = getTextLine(true); |
| |
| result += "<TextBlock>"; |
| result += textLine.textBlock.dump(); |
| result += "</TextBlock>"; |
| result += "<TextLine>" |
| result += textLine.dump(); |
| result += "</TextLine>" |
| |
| result += "</line>"; |
| return result; |
| } |
| |
| }; |
| } |
| |
| import flash.geom.Rectangle; |
| |
| import flashx.textLayout.edit.ISelectionManager; |
| |
| /** @private - I would have defined this as tlf_internal, but that is not an option, so |
| * making it private. |
| * |
| * The SelectionCache is a structure designed to hold a few key data points needed to quickly |
| * reconstruct a selection on a line: |
| * |
| * a) the beginning and end indicies of the selection on the line |
| * b) the regular selection rectangles |
| * c) the irregular selection rectangles, such as TCY selection rects in vertical text |
| * |
| **/ |
| final class SelectionCache |
| { |
| private var _begIdx:int = -1; |
| private var _endIdx:int = -1; |
| |
| private var _selectionBlocks:Array = null; |
| |
| public function SelectionCache() |
| { |
| } |
| |
| public function get begIdx():int |
| { return _begIdx; } |
| public function set begIdx(val:int):void |
| { _begIdx = val; } |
| |
| public function get endIdx():int |
| { return _endIdx; } |
| public function set endIdx(val:int):void |
| { _endIdx = val; } |
| |
| public function pushSelectionBlock(drawRect:Rectangle):void |
| { |
| if(!_selectionBlocks) |
| _selectionBlocks = new Array(); |
| |
| _selectionBlocks.push(drawRect.clone()); |
| } |
| |
| public function get selectionBlocks():Array |
| { return _selectionBlocks; } |
| |
| |
| public function clear():void |
| { |
| _selectionBlocks = null; |
| _begIdx = -1; |
| _endIdx = -1; |
| } |
| |
| } |