blob: e4972f1a2e41f56104cd309ef73178261ff67b0b [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.edit
{
import flash.geom.Point;
import flash.utils.getQualifiedClassName;
import flashx.textLayout.container.ContainerController;
import flashx.textLayout.debug.assert;
import flashx.textLayout.elements.FlowElement;
import flashx.textLayout.elements.FlowGroupElement;
import flashx.textLayout.elements.FlowLeafElement;
import flashx.textLayout.elements.InlineGraphicElement;
import flashx.textLayout.elements.ParagraphElement;
import flashx.textLayout.elements.SpanElement;
import flashx.textLayout.elements.SubParagraphGroupElementBase;
import flashx.textLayout.elements.TextFlow;
import flashx.textLayout.formats.Float;
import flashx.textLayout.formats.ITextLayoutFormat;
import flashx.textLayout.formats.TextLayoutFormat;
import flashx.textLayout.tlf_internal;
use namespace tlf_internal;
[ExcludeClass]
/**
* Encapsulates all methods necessary for dynamic editing of a text. The methods are all static member functions of this class.
* @private - because we can't make it tlf_internal. Used by the operations package
*/
public class ParaEdit
{
/**
* Inserts text into specified paragraph
* @param textFlow TextFlow to insert into
* @param absoluteStart index relative to beginning of the TextFlow to insert text
* @param text actual text to insert
* @param createNewSpan flag to force creation of a new span
*/
public static function insertText(textFlow:TextFlow, absoluteStart:int,insertText:String, createNewSpan:Boolean):SpanElement
{
if (insertText.length == 0)
return null; // error? other sanity checks needed here?
var sibling:FlowElement = textFlow.findLeaf(absoluteStart);
var siblingIndex:int;
var paragraph:ParagraphElement = sibling.getParagraph();
var paraStart:int = paragraph.getAbsoluteStart();
var paraSelBegIdx:int = absoluteStart - paraStart;
if (paraStart == absoluteStart) // insert to start of paragraph
siblingIndex = 0;
else
{
// If we're at the start a span, go to the previous span in the same paragraph, and insert at the end of it
if (paraSelBegIdx == sibling.getElementRelativeStart(paragraph))
sibling = FlowLeafElement(sibling).getPreviousLeaf(paragraph);
siblingIndex = sibling.parent.getChildIndex(sibling) + 1;
}
var insertParent:FlowGroupElement = sibling.parent;
// If we are adding text to the start or end of a link, it doesn't allow the insertion to group with the link.
// So in that case, we will insert to the element beside the position that is *not* part of the link.
var curSPGElement:SubParagraphGroupElementBase = sibling.getParentByType(SubParagraphGroupElementBase) as SubParagraphGroupElementBase;
while (curSPGElement != null)
{
var subParInsertionPoint:int = paraSelBegIdx - curSPGElement.getElementRelativeStart(paragraph);
if (((subParInsertionPoint == 0) && (!curSPGElement.acceptTextBefore())) ||
((!curSPGElement.acceptTextAfter() && (subParInsertionPoint == curSPGElement.textLength ||
(subParInsertionPoint == curSPGElement.textLength - 1 && (sibling == paragraph.getLastLeaf()))))))
{
createNewSpan = true;
sibling = insertParent;
insertParent = insertParent.parent;
curSPGElement = curSPGElement.getParentByType(SubParagraphGroupElementBase) as SubParagraphGroupElementBase;
siblingIndex = insertParent.getChildIndex(sibling) + 1;
} else {
break;
}
}
// adjust the flow so that we are in a span for the insertion
var insertSpan:SpanElement = sibling as SpanElement;
if (!insertSpan || createNewSpan)
{
var newSpan:SpanElement = new SpanElement();
var insertIdx:int;
if (siblingIndex > 0)
{
var relativeStart:int = paraSelBegIdx - sibling.getElementRelativeStart(paragraph);
if (createNewSpan)
{
if (relativeStart == 0)
siblingIndex--;
else if (relativeStart != sibling.textLength)
sibling.splitAtPosition(relativeStart); // we'll insert between the two elements
}
}
insertParent.replaceChildren(siblingIndex, siblingIndex, newSpan);
var formatElem:FlowLeafElement = newSpan.getPreviousLeaf(paragraph);
if (formatElem == null)
newSpan.format = newSpan.getNextLeaf(paragraph).format;
else
newSpan.format = formatElem.format;
insertSpan = newSpan;
}
var runInsertionPoint:int = paraSelBegIdx - insertSpan.getElementRelativeStart(paragraph);
insertSpan.replaceText(runInsertionPoint, runInsertionPoint, insertText);
return insertSpan;
}
private static function deleteTextInternal(para:ParagraphElement, paraSelBegIdx:int, totalToDelete:int):void
{
var composeNode:FlowElement;
var curSpan:SpanElement;
var curNumToDelete:int;
var curSpanDeletePos:int = 0;
while (totalToDelete > 0)
{
composeNode = para.findLeaf(paraSelBegIdx);
CONFIG::debug { assert(composeNode is SpanElement,"deleteTextInternal: leaf element is not a span"); }
curSpan = composeNode as SpanElement;
var curSpanRelativeStart:int = curSpan.getElementRelativeStart(para);
curSpanDeletePos = paraSelBegIdx - curSpanRelativeStart;
if (paraSelBegIdx > (curSpanRelativeStart + curSpan.textLength))
{
curNumToDelete = curSpan.textLength;
}
else
{
curNumToDelete = (curSpanRelativeStart + curSpan.textLength) - paraSelBegIdx;
}
if (totalToDelete < curNumToDelete)
{
curNumToDelete = totalToDelete;
}
curSpan.replaceText(curSpanDeletePos, curSpanDeletePos + curNumToDelete, "");
if (curSpan.textLength == 0)
{
var delIdx:int = curSpan.parent.getChildIndex(curSpan);
curSpan.parent.replaceChildren(delIdx,delIdx+1,null);
}
totalToDelete -= curNumToDelete;
}
}
public static function deleteText(para:ParagraphElement, paraSelBegIdx:int, totalToDelete:int):void
{
var lastParPos:int = para.textLength - 1;
if ((paraSelBegIdx < 0) || (paraSelBegIdx > lastParPos))
{
//not much we can do. There's nothing to delete in this paragraph
return;
}
if (totalToDelete <= 0)
{
//can't delete a negative number of characters... just return
return;
}
var endPos:int = paraSelBegIdx + totalToDelete - 1;
if (endPos > lastParPos)
{
endPos = lastParPos;
totalToDelete = endPos - paraSelBegIdx + 1
}
deleteTextInternal(para,paraSelBegIdx,totalToDelete);
}
/**
* Creates image and inserts it into specified FlowGroupElement
* @param flowBlock FlowGroupElement to insert image into
* @param flowSelBegIdx index relative to beginning of the FlowGroupElement to insert image
* @param urlString the url of image to insert
* @param width the width of the image
* @param height the height of the image
* @param options none supported
*/
public static function createImage(flowBlock:FlowGroupElement, flowSelBegIdx:int,source:Object, width:Object, height:Object, options:Object, pointFormat:ITextLayoutFormat):InlineGraphicElement
{
//first, split the element that we are on
var curComposeNode:FlowElement = flowBlock.findLeaf(flowSelBegIdx);
var posInCurComposeNode:int = 0;
if (curComposeNode != null)
{
posInCurComposeNode = flowSelBegIdx - curComposeNode.getElementRelativeStart(flowBlock); // curComposeNode.parentRelativeStart;
}
if ((curComposeNode != null) && (posInCurComposeNode > 0) && (posInCurComposeNode < curComposeNode.textLength))
{
//it is a LeafElement, and not position 0. It has to be a Span
(curComposeNode as SpanElement).splitAtPosition(posInCurComposeNode);
}
//the FlowElement or FlowGroupElement is now split. Insert the image now.
var imgElem:InlineGraphicElement = new InlineGraphicElement();
imgElem.height = height;
imgElem.width = width;
imgElem.float = options ? options.toString() : undefined;
var src:Object = source;
var embedStr:String = "@Embed";
if (src is String && src.length > embedStr.length && src.substr(0, embedStr.length) == embedStr) {
// we should be dealing with an embedded asset. They are of the form "url=@Embed(source='path-to-asset')"
var searchStr:String = "source=";
var index:int = src.indexOf(searchStr, embedStr.length);
if (index > 0) {
index += searchStr.length;
index = src.indexOf("'", index);
src = src.substring(index+1, src.indexOf("'", index+1));
}
}
imgElem.source = src;
while (curComposeNode && curComposeNode.parent != flowBlock)
{
curComposeNode = curComposeNode.parent;
}
var elementIdx:int = curComposeNode != null ? flowBlock.getChildIndex(curComposeNode) : flowBlock.numChildren;
if (curComposeNode && posInCurComposeNode > 0)
elementIdx++;
flowBlock.replaceChildren(elementIdx,elementIdx,imgElem);
//clone characterFormat from the left OR iff the the first element the right
var p:ParagraphElement = imgElem.getParagraph();
var attrElem:FlowLeafElement = imgElem.getPreviousLeaf(p);
if (!attrElem)
attrElem = imgElem.getNextLeaf(p);
CONFIG::debug { assert(attrElem != null, "no element to get attributes from"); }
if (attrElem.format || pointFormat)
{
var imageElemFormat:TextLayoutFormat = new TextLayoutFormat(attrElem.format);
if (pointFormat)
imageElemFormat.apply(pointFormat);
imgElem.format = imageElemFormat;
}
return imgElem;
}
/** Merge changed attributes into this
*/
static private function splitForChange(span:SpanElement,begIdx:int,rangeLength:int):SpanElement
{
var startOffset:int = span.getAbsoluteStart();
if (begIdx == startOffset && rangeLength == span.textLength)
return span;
// element must be split into spans
var elemToUpdate:SpanElement;
var origLength:int = span.textLength;
var begRelativeIdx:int = begIdx - startOffset;
if (begRelativeIdx > 0)
{
// We create an initial span to hold the text before the new span, then
// a following span for the specified range.
elemToUpdate = span.splitAtPosition(begRelativeIdx) as SpanElement;
if (begRelativeIdx + rangeLength < origLength)
elemToUpdate.splitAtPosition(rangeLength);
}
else
{
// The specified range falls at the start of the element, so this span is the
// one that's getting the new format.
span.splitAtPosition(rangeLength);
elemToUpdate = span;
}
return elemToUpdate;
}
private static function undefineDefinedFormats(target:TextLayoutFormat,undefineFormat:ITextLayoutFormat):void
{
if (undefineFormat)
{
// this is fairly rare so this operation is not optimizied
var tlfUndefineFormat:TextLayoutFormat;
if (undefineFormat is TextLayoutFormat)
tlfUndefineFormat = undefineFormat as TextLayoutFormat;
else
tlfUndefineFormat = new TextLayoutFormat(undefineFormat);
for (var prop:String in tlfUndefineFormat.styles)
target.setStyle(prop, undefined);
}
}
/**
* Apply formatting changes to a range of text in the FlowElement
*
* @param begIdx text index of first text in span
* @param rangeLength number of characters to modify
* @param applyFormat Character Format to apply to content
* @param undefineFormat Character Format to undefine to content
* @return begIdx + number of actual actual characters modified.
*/
static private function applyCharacterFormat(leaf:FlowLeafElement, begIdx:int, rangeLength:int, applyFormat:ITextLayoutFormat, undefineFormat:ITextLayoutFormat):int
{
var newFormat:TextLayoutFormat = new TextLayoutFormat(leaf.format);
if (applyFormat)
newFormat.apply(applyFormat);
undefineDefinedFormats(newFormat,undefineFormat);
return setCharacterFormat(leaf, newFormat, begIdx, rangeLength);
}
/**
* Set formatting to a range of text in the FlowElement
*
* @param format Character Format to apply to content
* @param begIdx text index of first text in span
* @param rangeLength number of characters to modify
* @return starting position of following span
*/
static private function setCharacterFormat(leaf:FlowLeafElement, format:ITextLayoutFormat, begIdx:int, rangeLength:int):int
{
var startOffset:int = leaf.getAbsoluteStart();
if (!(format is ITextLayoutFormat) || !TextLayoutFormat.isEqual(ITextLayoutFormat(format),leaf.format))
{
var para:ParagraphElement = leaf.getParagraph();
var paraStartOffset:int = para.getAbsoluteStart();
// clip rangeLength to the length of this span. Extend the rangeLength by one to include the terminator if
// it is in the span, and the end of the range abuts the terminator. That way the terminator will stay in the
// last span.
var begRelativeIdx:int = begIdx - startOffset;
if (begRelativeIdx + rangeLength > leaf.textLength)
rangeLength = leaf.textLength - begRelativeIdx;
if (begRelativeIdx + rangeLength == leaf.textLength - 1 && (leaf is SpanElement) && SpanElement(leaf).hasParagraphTerminator)
++rangeLength;
var elemToUpdate:FlowLeafElement
if (leaf is SpanElement)
elemToUpdate = splitForChange(SpanElement(leaf),begIdx,rangeLength);
else
{
CONFIG::debug { assert(rangeLength >= leaf.textLength,"unable to split non-span leaf"); }
elemToUpdate = leaf;
}
if (format is ITextLayoutFormat)
elemToUpdate.format = ITextLayoutFormat(format);
else
elemToUpdate.setStylesInternal(format);
return begIdx+rangeLength;
}
rangeLength = leaf.textLength;
return startOffset+rangeLength;
}
public static function applyTextStyleChange(flowRoot:TextFlow,begChange:int,endChange:int,applyFormat:ITextLayoutFormat,undefineFormat:ITextLayoutFormat):void
{
// TODO: this code only works for span's. Revisit when new FlowLeafElement types enabled
var workIdx:int = begChange;
while (workIdx < endChange)
{
var elem:FlowLeafElement = flowRoot.findLeaf(workIdx);
CONFIG::debug { assert(elem != null,"null FlowLeafElement found"); }
workIdx = applyCharacterFormat(elem,workIdx,endChange-workIdx,applyFormat,undefineFormat);
}
}
// used for undo of operation
public static function setTextStyleChange(flowRoot:TextFlow,begChange:int, endChange:int, coreStyle:ITextLayoutFormat):void
{
// TODO: this code only works for span's. Revisit when new FlowLeafElement types enabled
var workIdx:int = begChange;
while (workIdx < endChange)
{
var elem:FlowElement = flowRoot.findLeaf(workIdx);
CONFIG::debug { assert(elem != null,"null FlowLeafElement found"); }
workIdx = setCharacterFormat(FlowLeafElement(elem),coreStyle,workIdx,endChange-workIdx);
}
}
public static function splitElement(elem:FlowGroupElement, splitPos:int):FlowGroupElement
{
CONFIG::debug { assert(splitPos >= 0 && splitPos <= elem.textLength, "Invalid call to ParaEdit.splitElement"); }
var rslt:FlowGroupElement = elem.splitAtPosition(splitPos) as FlowGroupElement;
// rslt always follows elem
if (!(rslt is SubParagraphGroupElementBase))
{
// need to insure there is a paragraph and a span on each side
var rsltParagraph:FlowGroupElement = rslt;
while (!(rsltParagraph is ParagraphElement) && rsltParagraph.numChildren)
rsltParagraph = rsltParagraph.getChildAt(0) as FlowGroupElement;
var elemParagraph:FlowGroupElement = elem;
while (!(elemParagraph is ParagraphElement) && elemParagraph.numChildren)
elemParagraph = elemParagraph.getChildAt(elemParagraph.numChildren-1) as FlowGroupElement;
CONFIG::debug { assert (elemParagraph is ParagraphElement || rsltParagraph is ParagraphElement,"ParaEdit.splitElement didn't find at least one paragraph"); }
var p:ParagraphElement;
if (!(elemParagraph is ParagraphElement))
{
// clone rlstParagraph
p = rsltParagraph.shallowCopy() as ParagraphElement;
elemParagraph.addChild(p);
elemParagraph = p;
}
else if (!(rsltParagraph is ParagraphElement))
{
p = elemParagraph.shallowCopy() as ParagraphElement;
rsltParagraph.addChild(p);
rsltParagraph = p;
}
// if we have an empty before or after para need to make sure the formats got copied
if (elemParagraph.textLength <= 1)
{
elemParagraph.normalizeRange(0,elemParagraph.textLength);
elemParagraph.getLastLeaf().quickCloneTextLayoutFormat(rsltParagraph.getFirstLeaf());
}
else if (rsltParagraph.textLength <= 1)
{
rsltParagraph.normalizeRange(0,rsltParagraph.textLength);
rsltParagraph.getFirstLeaf().quickCloneTextLayoutFormat(elemParagraph.getLastLeaf());
}
}
return rslt;
}
// TODO: rewrite this method by moving the elements. This is buggy.
public static function mergeParagraphWithNext(para:ParagraphElement):Boolean
{
var indexOfPara:int = para.parent.getChildIndex(para);
// last can't merge
if (indexOfPara == para.parent.numChildren-1)
return false;
var nextPar:ParagraphElement = para.parent.getChildAt(indexOfPara + 1) as ParagraphElement;
// next is not a paragraph
if (nextPar == null)
return false;
// remove nextPar from its parent - do this first because it will require less updating of starts and lengths
para.parent.replaceChildren(indexOfPara+1,indexOfPara+2,null);
if (nextPar.textLength <= 1)
return true;
// move all the elements
while (nextPar.numChildren)
{
var elem:FlowElement = nextPar.getChildAt(0);
nextPar.replaceChildren(0,1,null);
para.replaceChildren(para.numChildren,para.numChildren,elem);
if ((para.numChildren > 1) && (para.getChildAt(para.numChildren - 2).textLength == 0))
{
//bug 1658164
//imagine that the last element of para is only a kParaTerminator (like a single
//span of length 1 that only contains a kParaTerminator, and you merge with the
//next paragraph. That kParaTerminator will move, leaving an empty leaf element
para.replaceChildren(para.numChildren - 2, para.numChildren - 1, null);
}
}
return true;
}
public static function cacheParagraphStyleInformation(flowRoot:TextFlow,begSel:int,endSel:int,undoArray:Array):void
{
while (begSel <= endSel && begSel >= 0)
{
var para:ParagraphElement = flowRoot.findLeaf(begSel).getParagraph();
// build an object holding the old style and format
var obj:Object = new Object();
obj.begIdx = para.getAbsoluteStart();
obj.endIdx = obj.begIdx + para.textLength - 1;
obj.attributes = new TextLayoutFormat(para.format);
undoArray.push(obj);
begSel = obj.begIdx + para.textLength;
}
}
/**
* Replace the existing paragraph attributes with the incoming attributes.
*
* @param flowRoot text flow where paragraphs are
* @param format attributes to apply
* @param beginIndex text index within the first paragraph in the range
* @param endIndex text index within the last paragraph in the range
*/
// used for undo of operation
public static function setParagraphStyleChange(flowRoot:TextFlow,begChange:int, endChange:int, format:ITextLayoutFormat):void
{
var beginPara:int = begChange;
while (beginPara <= endChange)
{
var para:ParagraphElement = flowRoot.findLeaf(beginPara).getParagraph();
para.format = format ? new TextLayoutFormat(format) : null;
beginPara = para.getAbsoluteStart() + para.textLength;
}
}
/**
* Additively apply the paragraph formating attributes to the paragraphs in the specified range.
* Each non-null field in the incoming format is copied into the existing paragraph attributes.
*
* @param flowRoot text flow where paragraphs are
* @param format attributes to apply
* @param beginIndex text index within the first paragraph in the range
* @param endIndex text index within the last paragraph in the range
*/
public static function applyParagraphStyleChange(flowRoot:TextFlow,begChange:int,endChange:int,applyFormat:ITextLayoutFormat,undefineFormat:ITextLayoutFormat):void
{
var curIndex:int = begChange;
while (curIndex <= endChange)
{
var leaf:FlowLeafElement = flowRoot.findLeaf(curIndex);
if (!leaf)
break;
var para:ParagraphElement = leaf.getParagraph();
// now, need to get the change from "format" and apply to para. We make
// a new ParagraphFormat object instead of changing the ParagraphFormat
// already in the paragraph so that if the object is shared, other uses
// in other paragraphs will not be affected.
var newFormat:TextLayoutFormat = new TextLayoutFormat(para.format);
if (applyFormat)
newFormat.apply(applyFormat);
undefineDefinedFormats(newFormat,undefineFormat);
para.format = newFormat;
curIndex = para.getAbsoluteStart() + para.textLength;
}
}
public static function cacheStyleInformation(flowRoot:TextFlow,begSel:int,endSel:int,undoArray:Array):void
{
var elem:FlowElement = flowRoot.findLeaf(begSel);
var elemLength:int = elem.getAbsoluteStart()+elem.textLength-begSel;
var countRemaining:int = endSel - begSel;
CONFIG::debug { assert(countRemaining != 0,"cacheStyleInformation called on point selection"); }
for (;;)
{
// build an object holding the old style and format
var obj:Object = new Object();
obj.begIdx = begSel;
var objLength:int = Math.min(countRemaining, elemLength);
obj.endIdx = begSel + objLength;
// just the styles
obj.style = new TextLayoutFormat(elem.format);
undoArray.push(obj);
countRemaining -= Math.min(countRemaining, elemLength);
if (countRemaining == 0)
break;
// advance
begSel = obj.endIdx;
elem = flowRoot.findLeaf(begSel);
elemLength = elem.textLength;
}
}
public static function cacheContainerStyleInformation(flowRoot:TextFlow,begIdx:int,endIdx:int,undoArray:Array):void
{
CONFIG::debug { assert(begIdx <= endIdx,"bad indexeds passed to ParaEdit.cacheContainerStyleInformation"); }
if (flowRoot.flowComposer)
{
var startIdx:int = flowRoot.flowComposer.findControllerIndexAtPosition(begIdx,false);
if (startIdx == -1)
return;
var endIdx:int = flowRoot.flowComposer.findControllerIndexAtPosition(endIdx,true);
if (endIdx == -1)
endIdx = flowRoot.flowComposer.numControllers-1;
while (startIdx <= endIdx)
{
var controller:ContainerController = flowRoot.flowComposer.getControllerAt(startIdx);
var obj:Object = new Object();
obj.container = controller;
// save just the styles
obj.attributes = new TextLayoutFormat(controller.format);
undoArray.push(obj);
startIdx++;
}
}
}
public static function applyContainerStyleChange(flowRoot:TextFlow,begIdx:int,endIdx:int,applyFormat:ITextLayoutFormat,undefineFormat:ITextLayoutFormat):void
{
CONFIG::debug { assert(begIdx <= endIdx,"bad indexes passed to ParaEdit.cacheContainerStyleInformation"); }
if (flowRoot.flowComposer)
{
var startIdx:int = flowRoot.flowComposer.findControllerIndexAtPosition(begIdx,false);
if (startIdx == -1)
return;
var endIdx:int = flowRoot.flowComposer.findControllerIndexAtPosition(endIdx,true);
if (endIdx == -1)
endIdx = flowRoot.flowComposer.numControllers-1;
var controllerIndex:int = flowRoot.flowComposer.findControllerIndexAtPosition(begIdx,false);
while (startIdx <= endIdx)
{
var controller:ContainerController = flowRoot.flowComposer.getControllerAt(startIdx);
var newFormat:TextLayoutFormat = new TextLayoutFormat(controller.format);
if (applyFormat)
newFormat.apply(applyFormat);
undefineDefinedFormats(newFormat,undefineFormat);
controller.format = newFormat;
startIdx++;
}
}
}
/** obj is created by cacheContainerStyleInformation */
public static function setContainerStyleChange(obj:Object):void
{
obj.container.format = obj.attributes as ITextLayoutFormat;
}
}
}