//////////////////////////////////////////////////////////////////////////////// // // 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.desktop.Clipboard; import flash.desktop.ClipboardFormats; import flash.display.DisplayObject; import flash.display.InteractiveObject; import flash.display.Stage; import flash.errors.IllegalOperationError; import flash.events.ContextMenuEvent; import flash.events.Event; import flash.events.FocusEvent; import flash.events.IMEEvent; import flash.events.KeyboardEvent; import flash.events.MouseEvent; import flash.events.TextEvent; import flash.geom.Point; import flash.geom.Rectangle; import flash.text.engine.TextLine; import flash.text.engine.TextLineValidity; import flash.text.engine.TextRotation; import flash.ui.ContextMenu; import flash.ui.Keyboard; import flash.ui.Mouse; import flash.ui.MouseCursor; import flash.utils.getQualifiedClassName; import flashx.textLayout.compose.TextFlowLine; import flashx.textLayout.container.ColumnState; import flashx.textLayout.container.ContainerController; import flashx.textLayout.debug.Debugging; import flashx.textLayout.debug.assert; import flashx.textLayout.elements.FlowLeafElement; import flashx.textLayout.elements.GlobalSettings; import flashx.textLayout.elements.InlineGraphicElement; import flashx.textLayout.elements.ParagraphElement; import flashx.textLayout.elements.TextFlow; import flashx.textLayout.elements.TextRange; import flashx.textLayout.events.FlowOperationEvent; import flashx.textLayout.events.SelectionEvent; import flashx.textLayout.formats.BlockProgression; import flashx.textLayout.formats.Category; import flashx.textLayout.formats.Direction; import flashx.textLayout.formats.ITextLayoutFormat; import flashx.textLayout.formats.TextLayoutFormat; import flashx.textLayout.operations.CopyOperation; import flashx.textLayout.operations.FlowOperation; import flashx.textLayout.property.Property; import flashx.textLayout.tlf_internal; import flashx.textLayout.utils.NavigationUtil; use namespace tlf_internal; /** * The SelectionManager class manages text selection in a text flow. * *

The selection manager keeps track of the selected text range, manages its formatting, * and can handle events affecting the selection. To allow a user to make selections in * a text flow, assign a SelectionManager object to the interactionManager * property of the flow. (To allow editing, assign an instance of the EditManager class, * which extends SelectionManager.)

* *

The following table describes how the SelectionManager class handles keyboard shortcuts:

* * * * * * * * * * * * * * *
TB,LTRTB,RTLTL,LTRRL,RTL
nonectrlalt|ctrl+altnonectrlalt|ctrl+altnonectrlalt|ctrl+altnonectrlalt|ctrl+alt
leftarrowpreviousCharacterpreviousWordpreviousWordnextCharacternextWordnextWordnextLineendOfDocumentendOfParagraphnextLineendOfDocumentendOfParagraph
uparrowpreviousLinestartOfDocumentstartOfParagraphpreviousLinestartOfDocumentstartOfParagraphpreviousCharacterpreviousWordpreviousWordnextCharacternextWordnextWord
rightarrownextCharacternextWordnextWordpreviousCharacterpreviousWordpreviousWordpreviousLinestartOfDocumentstartOfParagraphpreviousLinestartOfDocumentstartOfParagraph
downarrownextLineendOfDocumentendOfParagraphnextLineendOfDocumentendOfParagraphnextCharacternextWordnextWordpreviousCharacterpreviousWordpreviousWord
homestartOfLinestartOfDocumentstartOfLinestartOfLinestartOfDocumentstartOfLinestartOfLinestartOfDocumentstartOfLinestartOfLinestartOfDocumentstartOfLine
endendOfLineendOfDocumentendOfLineendOfLineendOfDocumentendOfLineendOfLineendOfDocumentendOfLineendOfLineendOfDocumentendOfLine
pagedownnextPagenextPagenextPagenextPagenextPagenextPagenextPagenextPagenextPagenextPagenextPagenextPage
pageuppreviousPagepreviousPagepreviousPagepreviousPagepreviousPagepreviousPagepreviousPagepreviousPagepreviousPagepreviousPagepreviousPagepreviousPage
* *

Key: *

* * @see EditManager * @see flashx.elements.TextFlow * * @includeExample examples\SelectionManager_example.as -noswf * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public class SelectionManager implements ISelectionManager { private var _focusedSelectionFormat:SelectionFormat; private var _unfocusedSelectionFormat:SelectionFormat; private var _inactiveSelectionFormat:SelectionFormat; private var _selFormatState:String = SelectionFormatState.UNFOCUSED; private var _isActive:Boolean; /** The TextFlow of the selection. */ private var _textFlow:TextFlow; // current range of selection /** Anchor point of the current selection, as an index into the TextFlow. */ private var anchorMark:Mark; /** Active end of the current selection, as an index into the TextFlow. */ private var activeMark:Mark; // used to save pending attributes at a point selection private var _pointFormat:ITextLayoutFormat; /** * The format that will be applied to inserted text. * * TBD: pointFormat needs to be extended to remember user styles and "undefine" of formats from calls to IEditManager.undefineFormat with leafFormat values on a point selection. */ protected function get pointFormat():ITextLayoutFormat { return _pointFormat; } /** @private * Ignore the next text input event. This is needed because the player may send a text input event * following by a key down event when ctrl+key is entered. */ protected var ignoreNextTextEvent:Boolean = false; /** * @private * For usability reasons, operations are sometimes grouped (merged) so they * can be undone together. Certain events, such as changing the selection, may make merging * inappropriate. This flag is used to keep track of when operation merging * is appropriate. This might need to be moved to SelectionManager later. I'm keeping it * here for now since I'm unsure if other regular selection operations that we add can * be undone. */ protected var allowOperationMerge:Boolean = false; private var _mouseOverSelectionArea:Boolean = false; CONFIG::debug { protected var id:String; static private var smCount:int = 0; } /** * * Creates a SelectionManager object. * *

Assign a SelectionManager object to the interactionManager property of * a text flow to enable text selection.

* * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function SelectionManager() { _textFlow = null; anchorMark = createMark(); activeMark = createMark(); _pointFormat = null; _isActive = false; CONFIG::debug { this.id = smCount.toString(); smCount++; } } /** * @copy ISelectionManager#getSelectionState() * * @includeExample examples\SelectionManager_getSelectionState.as -noswf * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 * * @see flashx.textLayout.edit.SelectionState */ public function getSelectionState():SelectionState { return new SelectionState(_textFlow, anchorMark.position, activeMark.position, pointFormat); } /** * @copy ISelectionManager#setSelectionState() * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 * * @see flashx.textLayout.edit.SelectionState */ public function setSelectionState(sel:SelectionState):void { internalSetSelection(sel.textFlow, sel.anchorPosition, sel.activePosition, sel.pointFormat); } /** * @copy ISelectionManager#hasSelection() * * @includeExample examples\SelectionManager_hasSelection.as -noswf * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function hasSelection():Boolean { return anchorMark.position != -1; } /** * @copy ISelectionManager#isRangeSelection() * * @includeExample examples\SelectionManager_isRangeSelection.as -noswf * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function isRangeSelection():Boolean { return anchorMark.position != -1 && anchorMark.position != activeMark.position; } /** * The TextFlow object managed by this selection manager. * *

A selection manager manages a single text flow. A selection manager can also be * assigned to a text flow by setting the interactionManager property of the * TextFlow object.

* * @see flashx.textLayout.elements.TextFlow#interactionManager * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function get textFlow():TextFlow { return _textFlow; } public function set textFlow(value:TextFlow):void { if (_textFlow != value) { if (_textFlow) flushPendingOperations(); clear(); _textFlow = value; if (_textFlow && _textFlow.interactionManager != this) _textFlow.interactionManager = this; } } /** * @copy ISelectionManager#editingMode * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 * * @see flashx.textLayout.edit.EditingMode */ public function get editingMode():String { return EditingMode.READ_SELECT; } /** * @copy ISelectionManager#windowActive * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function get windowActive():Boolean { return _selFormatState != SelectionFormatState.INACTIVE; } /** * @copy ISelectionManager#focused * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function get focused():Boolean { return _selFormatState == SelectionFormatState.FOCUSED; } /** * @copy ISelectionManager#currentSelectionFormat * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 * * @see flashx.textLayout.edit.SelectionFormat */ public function get currentSelectionFormat():SelectionFormat { if (_selFormatState == SelectionFormatState.UNFOCUSED) { return unfocusedSelectionFormat; } else if (_selFormatState == SelectionFormatState.INACTIVE) { return inactiveSelectionFormat; } return focusedSelectionFormat; } /** * @copy ISelectionManager#focusedSelectionFormat * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 * * @see flashx.textLayout.edit.SelectionFormat */ public function set focusedSelectionFormat(val:SelectionFormat):void { _focusedSelectionFormat = val; if (this._selFormatState == SelectionFormatState.FOCUSED) refreshSelection(); } /** * @private - docs on setter */ public function get focusedSelectionFormat():SelectionFormat { return _focusedSelectionFormat ? _focusedSelectionFormat : (_textFlow ? _textFlow.configuration.focusedSelectionFormat : null); } /** * @copy ISelectionManager#unfocusedSelectionFormat * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 * * @see flashx.textLayout.edit.SelectionFormat */ public function set unfocusedSelectionFormat(val:SelectionFormat):void { _unfocusedSelectionFormat = val; if (this._selFormatState == SelectionFormatState.UNFOCUSED) refreshSelection(); } /** * @private - docs on setter */ public function get unfocusedSelectionFormat():SelectionFormat { return _unfocusedSelectionFormat ? _unfocusedSelectionFormat : (_textFlow ? _textFlow.configuration.unfocusedSelectionFormat : null); } /** * @copy ISelectionManager#inactiveSelectionFormat * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 * * @see flashx.textLayout.edit.SelectionFormat */ public function set inactiveSelectionFormat(val:SelectionFormat):void { _inactiveSelectionFormat = val; if (this._selFormatState == SelectionFormatState.INACTIVE) refreshSelection(); } /** * @private - docs on setter */ public function get inactiveSelectionFormat():SelectionFormat { return _inactiveSelectionFormat ? _inactiveSelectionFormat : (_textFlow ? _textFlow.configuration.inactiveSelectionFormat : null); } /** @private - returns the selectionFormatState. @see flashx.textLayout.edit.SelectionFormatState */ tlf_internal function get selectionFormatState():String { return _selFormatState; } /** @private - sets the SelectionFormatState. @see flashx.textLayout.edit.SelectionFormatState */ tlf_internal function setSelectionFormatState(selFormatState:String):void { if (selFormatState != _selFormatState) { // trace("changing selection state: was", _selFormatState, "switching to", selFormatState, "on selectionManager", id); var oldSelectionFormat:SelectionFormat = currentSelectionFormat; _selFormatState = selFormatState; var newSelectionFormat:SelectionFormat = currentSelectionFormat; if (!newSelectionFormat.equals(oldSelectionFormat)) { refreshSelection(); } } } /** @private */ tlf_internal function cloneSelectionFormatState(oldISelectionManager:ISelectionManager):void { var oldSelectionManager:SelectionManager = oldISelectionManager as SelectionManager; if (oldSelectionManager) { _isActive = oldSelectionManager._isActive; _mouseOverSelectionArea = oldSelectionManager._mouseOverSelectionArea; setSelectionFormatState(oldSelectionManager.selectionFormatState); } } /** * Gets the SelectionState at the specified mouse position. * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 * @see flashx.textLayout.edit.SelectionState * @param currentTarget The object that is actively processing the Event object with an event listener. * @param target The InteractiveObject instance under the pointing device. The target is not always the object in the display list that registered the event listener. Use the currentTarget property to access the object in the display list that is currently processing the event. * @param localX The horizontal coordinate at which the event occurred relative to the containing sprite. * @param localY The vertical coordinate at which the event occurred relative to the containing sprite. * @param extendSelection Indicates that only activeIndex should move * @return the resulting SelectionState */ private function selectionPoint(currentTarget:Object, target:InteractiveObject, localX:Number, localY:Number, extendSelection:Boolean = false):SelectionState { //trace("selectionPoint"); if (!_textFlow) return null; if (!hasSelection()) extendSelection = false; var begIdx:int = anchorMark.position; var endIdx:int = activeMark.position; endIdx = computeSelectionIndex(_textFlow, target, currentTarget, localX, localY); if (endIdx == -1) return null; // ignore // attempt to position in the logical hierarchy var leaf:FlowLeafElement = _textFlow.findLeaf(endIdx); // make sure we aren't selecting after the paragraph terminating character var para:ParagraphElement = leaf.getParagraph(); if (endIdx == para.getAbsoluteStart() + para.textLength) endIdx--; if (!extendSelection) begIdx = endIdx; if (begIdx == endIdx) { if (leaf is InlineGraphicElement) { endIdx++; } begIdx = NavigationUtil.updateStartIfInReadOnlyElement(_textFlow, begIdx); endIdx = NavigationUtil.updateEndIfInReadOnlyElement(_textFlow, endIdx); } else { endIdx = NavigationUtil.updateEndIfInReadOnlyElement(_textFlow, endIdx); } return new SelectionState(textFlow, begIdx, endIdx); } /** * @copy ISelectionManager#setFocus() * * @includeExample examples\SelectionManager_setFocus.as -noswf * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function setFocus():void { if (hasSelection()) { // trace("setFocus sm", id); // container with the activePosition gets the key focus if (_textFlow.flowComposer) _textFlow.flowComposer.setFocus(activePosition,false); setSelectionFormatState(SelectionFormatState.FOCUSED); } } /** * @copy ISelectionManager#anchorPosition */ public function get anchorPosition() : int { return anchorMark.position; } /** * @copy ISelectionManager#activePosition * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function get activePosition() : int { return activeMark.position; } /** * @copy ISelectionManager#absoluteStart * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function get absoluteStart() : int { return (anchorMark.position < activeMark.position) ? anchorMark.position : activeMark.position; } /** * @copy ISelectionManager#absoluteEnd * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function get absoluteEnd() : int { return (anchorMark.position > activeMark.position) ? anchorMark.position : activeMark.position; } /** * @copy ISelectionManager#selectAll * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 * * @see flashx.textLayout.compose.IFlowComposer */ public function selectAll() : void { selectRange(0, int.MAX_VALUE); } /** * @copy ISelectionManager#selectRange * * @includeExample examples\SelectionManager_selectRange.as -noswf * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 * * @see flashx.textLayout.compose.IFlowComposer */ public function selectRange(anchorPosition:int, activePosition:int) : void { flushPendingOperations(); // anchor and active can be in any order // TODO: range check and clamp anchor,active if (anchorPosition != anchorMark.position || activePosition != activeMark.position) { clearSelectionShapes(); internalSetSelection(_textFlow, anchorPosition, activePosition); // selection changed selectionChanged(); allowOperationMerge = false; } } private function internalSetSelection(root:TextFlow,anchorPosition:int,activePosition:int,format:ITextLayoutFormat = null) : void { _textFlow = root; // clamp anchor/active if (anchorPosition < 0 || activePosition < 0) { anchorPosition = -1; activePosition = -1; } if (anchorPosition != -1 && activePosition != -1) { if (anchorPosition >= _textFlow.textLength) anchorPosition = _textFlow.textLength - 1; if (activePosition >= _textFlow.textLength) activePosition = _textFlow.textLength - 1; } _pointFormat = format; anchorMark.position = anchorPosition; // NavigationUtil.updateStartIfInReadOnlyElement(root, anchorPosition); activeMark.position = activePosition; // NavigationUtil.updateEndIfInReadOnlyElement(root, activePosition); // trace("Selection ", anchorMark, "to", activeMark.position); } /** Clear any active selections. */ private function clear(): void { if (hasSelection()) { flushPendingOperations(); clearSelectionShapes(); internalSetSelection(_textFlow, -1, -1); // selection cleared selectionChanged(); allowOperationMerge = false; } } private function addSelectionShapes():void { var blinking:Boolean = (currentSelectionFormat.pointBlinkRate != 0); if (_textFlow.flowComposer && (blinking || (!blinking && (activeMark.position != anchorMark.position)))) { // zero alpha means nothing is drawn so skip it if (currentSelectionFormat && (((absoluteStart == absoluteEnd) && (currentSelectionFormat.pointAlpha != 0)) || ((absoluteStart != absoluteEnd) && (currentSelectionFormat.rangeAlpha != 0)))) { var containerIter:int = 0; while(containerIter < _textFlow.flowComposer.numControllers) { _textFlow.flowComposer.getControllerAt(containerIter++).addSelectionShapes(currentSelectionFormat, absoluteStart, absoluteEnd); } } } } private function clearSelectionShapes():void { if (_textFlow.flowComposer) { var containerIter:int = 0; while(containerIter < _textFlow.flowComposer.numControllers) { _textFlow.flowComposer.getControllerAt(containerIter++).clearSelectionShapes(); } } } /** * @copy ISelectionManager#refreshSelection() * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function refreshSelection(): void { if (hasSelection()) { clearSelectionShapes(); addSelectionShapes(); } } /** Verifies that the selection is in a legal state. @private */ CONFIG::debug public function debugCheckSelectionManager():int { var rslt:int = 0; if (hasSelection()) { // both points must be within the flow - may not include trailing \n in final paragraph rslt+= assert(anchorMark.position >= 0 && anchorMark.position < _textFlow.textLength,"SelectionManager:validate selBegIdx is out of range"); rslt += assert(activeMark.position >= 0 && activeMark.position < _textFlow.textLength,"SelectionManager:validate selEndIdx is out of range"); } return rslt; } // //////////////////////////////////// // internal selection handling methods // //////////////////////////////////// /** @private * Handler function called when the selection has been changed. * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 * @param doDispatchEvent true if a selection changed event will be sent * @param resetPointFormat true if the attributes associated with the caret should be discarded */ tlf_internal function selectionChanged(doDispatchEvent:Boolean = true, resetPointFormat:Boolean=true):void { CONFIG::debug { debugCheckSelectionManager(); } // validates the selection // clear any remembered attributes for the next character if (resetPointFormat) _pointFormat = null; if (doDispatchEvent) textFlow.dispatchEvent(new SelectionEvent(SelectionEvent.SELECTION_CHANGE, false, false, hasSelection() ? getSelectionState() : null)); } // TODO: this routine could be much more efficient - instead of iterating over all lines in the TextFlow it should iterate over // the visible lines in the container. Todo that move this routine into ContainerController and use the shapeChildren along with the logic in fillShapeChildren static private function computeSelectionIndexInContainer(textFlow:TextFlow, controller:ContainerController, localX:Number, localY:Number):int { //var origX:Number = localX; //var origY:Number = localY; var lineIndex:int = -1; var firstCharVisible:int = controller.absoluteStart; var length:int = controller.textLength; // try to find a point on the line var bp:String = textFlow.computedFormat.blockProgression; var isTTB:Boolean = (bp == BlockProgression.RL); var isDirectionRTL:Boolean = (textFlow.computedFormat.direction == Direction.RTL); //Establish perpendicular the coordinate for use with TTB or LTR/RTL lines var perpCoor:Number = isTTB ? localX : localY; //get the nearest column so we can ignore lines which aren't in the column we're looking for. //if we don't do this, we won't be able to select across column boundaries. var nearestColIdx:int = locateNearestColumn(controller, localX, localY, textFlow.computedFormat.blockProgression,textFlow.computedFormat.direction); var prevLineBounds:Rectangle = null; var previousLineIndex:int = -1; var lastLineIndexInColumn:int = -1; // Matching TextFlowLine and TextLine - they are not necessarily valid var rtline:TextFlowLine; var rtTextLine:TextLine; for (var testIndex:int = textFlow.flowComposer.numLines - 1; testIndex >= 0; testIndex--) { rtline = textFlow.flowComposer.getLineAt(testIndex); if (rtline.controller != controller || rtline.columnIndex != nearestColIdx) { // use last line in previous column if (lastLineIndexInColumn != -1) { lineIndex = testIndex+1; break; } continue; } // is this line even displayed? if (rtline.absoluteStart < firstCharVisible || rtline.absoluteStart >= firstCharVisible+length) continue; rtTextLine = rtline.getTextLine(); if (rtTextLine == null || rtTextLine.parent == null) continue; if (lastLineIndexInColumn == -1) lastLineIndexInColumn = testIndex; var bounds:Rectangle = rtTextLine.getBounds(DisplayObject(controller.container)); // trace(testIndex.toString(),":",bounds.toString()); var linePerpCoor:Number = isTTB ? bounds.left : bounds.bottom; var midPerpCoor:Number = -1;//will be a positive value if prevLineBounds is not null //if this is not the first test loop, use the prevLineBounds to find the mid-point between the current //line, which will be logically up from the previous line - we're walking back-to-front if(prevLineBounds) { //if it's ttb, use the right bounds (ie the top of the line)... var prevPerpCoor:Number = (isTTB ? prevLineBounds.right : prevLineBounds.top); //calculate the midpoint midPerpCoor = (linePerpCoor + prevPerpCoor)/2; } //if the current line is below the click, then this OR the previous line, is the line we're looking for var isLineBelow:Boolean = (isTTB ? linePerpCoor > perpCoor : linePerpCoor < perpCoor); if(isLineBelow || testIndex == 0) { //if we haven't calculated the midPerpCoor (-1), then this is the first loop and we want to use the //current line,. Otherwise, if the click's perpendicular coordinate is below the mid point between the current //line or below it, then we want to use the line below (ie the previous line, but logically the one after the current) var inPrevLine:Boolean = midPerpCoor != -1 && (isTTB ? perpCoor < midPerpCoor : perpCoor > midPerpCoor); lineIndex = inPrevLine && testIndex != lastLineIndexInColumn ? testIndex+1 : testIndex; break; } else { //this line is below the click, so set the prevLineBounds to bounds of the current line and move on... prevLineBounds = bounds; previousLineIndex = testIndex; } } if (lineIndex == -1) { lineIndex = previousLineIndex; if (lineIndex == -1) return -1; // no lines in container } //Get a valid textLine -- check to make sure line is valid, regenerate if necessary, make sure it has correct container relative coordinates var tfl:TextFlowLine = textFlow.flowComposer.getLineAt(lineIndex); var textLine:TextLine = tfl.getTextLine(true); // adjust localX,localY to be relative to the textLine. // Can't use localToGlobal/globalToLocal because textLine may not be on the display list due to virtualization // we may need to bring this back if textline's can be rotated or placed by any mechanism other than a translation // but then we'll need to provisionally place a virtualized TextLine in its parent container localX -= textLine.x; localY -= textLine.y; /* var localPoint:Point = DisplayObject(controller.container).localToGlobal(new Point(localX,localY)); localPoint = textLine.globalToLocal(localPoint); localX = localPoint.x; localY = localPoint.y; */ var lastAtomRect:Rectangle = new Rectangle(0, 0, 0, 0); var textFlowLine:TextFlowLine = TextFlowLine(textLine.userData); var startOnNextLineIfNecessary:Boolean = false; if (isDirectionRTL) { lastAtomRect = textLine.getAtomBounds(textLine.atomCount - 1); } else { if ((textFlowLine.absoluteStart + textFlowLine.textLength) >= textFlowLine.paragraph.textLength) { if (textLine.atomCount > 1) lastAtomRect = textLine.getAtomBounds(textLine.atomCount - 2); } else { var lastLinePosInPar:int = textFlowLine.absoluteStart + textFlowLine.textLength - 1; var lastChar:String = textLine.textBlock.content.rawText.charAt(lastLinePosInPar); if (lastChar == " ") { if (textLine.atomCount > 1) lastAtomRect = textLine.getAtomBounds(textLine.atomCount - 2); } else { startOnNextLineIfNecessary = true; if (textLine.atomCount > 0) lastAtomRect = textLine.getAtomBounds(textLine.atomCount - 1); } } } if (!isTTB) { if (localX < 0) localX = 0; else if (localX > (lastAtomRect.x + lastAtomRect.width)) { if (startOnNextLineIfNecessary) return textFlowLine.absoluteStart + textFlowLine.textLength; localX = lastAtomRect.x + lastAtomRect.width; } } else { if (localY < 0) localY = 0; else if (localY > (lastAtomRect.y + lastAtomRect.height)) { if (startOnNextLineIfNecessary) return textFlowLine.absoluteStart + textFlowLine.textLength; localY = lastAtomRect.y + lastAtomRect.height; } } var result:int = computeSelectionIndexInLine(textFlow, textLine, localX, localY); // trace("computeSelectionIndexInContainer:(",origX,origY,")",textFlow.flowComposer.getControllerIndex(controller).toString(),lineIndex.toString(),result.toString()); return result != -1 ? result : firstCharVisible + length; } static private function locateNearestColumn(container:ContainerController, localX:Number, localY:Number, wm:String, direction:String):int { var colIdx:int = 0; //if we only have 1 column, no need to perform calculation... var columnState:ColumnState = container.columnState; //we need to compare the current column to the nextColmn while(colIdx < columnState.columnCount - 1) { var curCol:Rectangle = columnState.getColumnAt(colIdx); var nextCol:Rectangle = columnState.getColumnAt(colIdx + 1); if(curCol.contains(localX, localY)) //in current column break; if(nextCol.contains(localX, localY))//in next column { ++colIdx; break; } else { if(wm == BlockProgression.RL) { //if localY is above curCol || between columns, but close to current if(localY < curCol.top || localY < nextCol.top && Math.abs(curCol.bottom - localY) <= Math.abs(nextCol.top - localY)) break; if(localY > nextCol.top)//between but closer to nextCol { ++colIdx; break; } } else { if(direction == Direction.LTR) { //if localX is left of curCol || between columns but closer to current, break here if(localX < curCol.left || localX < nextCol.left && Math.abs(curCol.right - localX) <= Math.abs(nextCol.left - localX)) break; if(localX < nextCol.left) // between, but closer to next column { ++colIdx; break; } } else { //if localX is right of curCol || between columns, but closer to current if(localX > curCol.right || localX > nextCol.right && Math.abs(curCol.left - localX) <= Math.abs(nextCol.right - localX)) break; if(localX > nextCol.right) // between, but closer to next column { ++colIdx; break; } } } } //increment colIdx. If this is the last pass through, then the conditions above were never met //so we want the last column ++colIdx; } return colIdx; } static private function computeSelectionIndexInLine(textFlow:TextFlow, textLine:TextLine,localX:Number,localY:Number):int { if (!(textLine.userData is TextFlowLine)) return -1; // not a TextLayout generated line var rtline:TextFlowLine = TextFlowLine(textLine.userData); if (rtline.validity == TextLineValidity.INVALID) return -1; // not currently composed textLine = rtline.getTextLine(true); // make sure the TextLine is not released var isTTB:Boolean = textFlow.computedFormat.blockProgression == BlockProgression.RL; var perpCoor:Number = isTTB ? localX : localY; // new code for builds 385 and later var pt:Point = new Point(); pt.x = localX; pt.y = localY; // in most cases, we want to "fixup" the coordiates of the x and y coordinates //because we could be getting a positive results for a click in the line, but the //coordinates do not match any particular glyph. However, there are cases where the //fix leads to bad results. For example, if there is a TCY run, this code will always cause //a selection to be created in the middle of the run, meaning idividual glyphs cannot be selected. // //As a result, we need to be performing the less common case check prior to adjusting the //coordinates. pt = textLine.localToGlobal(pt); var elemIdx:int = textLine.getAtomIndexAtPoint(pt.x,pt.y); //trace("global point: " + pt); //trace("elemIdx: " + elemIdx); if(elemIdx == -1) { //reset the pt pt.x = localX; pt.y = localY; //make adjustments if (pt.x < 0 || (isTTB && perpCoor > textLine.ascent)) pt.x = 0; if (pt.y < 0 || (!isTTB && perpCoor > textLine.descent)) pt.y = 0; //get the global again and get try for the element again pt = textLine.localToGlobal(pt); elemIdx = textLine.getAtomIndexAtPoint(pt.x,pt.y); //trace("global point (second): " + pt); //trace("elemIdx (second): " + elemIdx); } //now we REALLY don't have a glyph, so return the head or tail of the line. if (elemIdx == -1) { //we need to use global coordinates here. reset pt and get conversion... pt.x = localX; pt.y = localY; pt = textLine.localToGlobal(pt) if(!isTTB) return (pt.x <= textLine.x) ? rtline.absoluteStart : (rtline.absoluteStart + rtline.textLength - 1); else return (pt.y <= textLine.y) ? rtline.absoluteStart : (rtline.absoluteStart + rtline.textLength - 1); } // get the character box and if check we are past the middle select past this character. var glyphRect:Rectangle = textLine.getAtomBounds(elemIdx); // trace("idx",elemIdx,"x",glyphRect.x,"y",glyphRect.y,"width",glyphRect.width,"height",glyphRect.height,"localX",localX,"localY",localY,"textLine.x",textLine.x); var leanRight:Boolean = false; if(glyphRect) { //if this is TTB and NOT TCY determine lean based on Y coordinates... if(isTTB && textLine.getAtomTextRotation(elemIdx) != TextRotation.ROTATE_0) leanRight = (localY > (glyphRect.y + glyphRect.height/2)); else //use X.. leanRight = (localX > (glyphRect.x + glyphRect.width/2)); } var paraSelectionIdx:int; if ((textLine.getAtomBidiLevel(elemIdx) % 2) != 0) // Right to left case, right is "start" unicode paraSelectionIdx = leanRight ? textLine.getAtomTextBlockBeginIndex(elemIdx) : textLine.getAtomTextBlockEndIndex(elemIdx); else // Left to right case, right is "end" unicode paraSelectionIdx = leanRight ? textLine.getAtomTextBlockEndIndex(elemIdx) : textLine.getAtomTextBlockBeginIndex(elemIdx); //we again need to do some fixup here. Unfortunately, we don't have the index into the paragraph until return rtline.paragraph.getAbsoluteStart() + paraSelectionIdx; } static private function checkForDisplayed(container:DisplayObject):Boolean { try { while (container) { if (!container.visible) return false; container = container.parent; if (container is Stage) return true; } } catch (e:Error) { return true; } return false; // not on the stage } /** @private - given a target and location compute the selectionIndex */ static tlf_internal function computeSelectionIndex(textFlow:TextFlow, target:Object, currentTarget:Object, localX:Number,localY:Number):int { //trace("computeSelectionIndex"); var rslt:int = 0; var containerPoint:Point; // scratch //Make sure that if the target is a line, that it is part of THIS textFlow and not another. Can happen //when holding down mouse and moving out of one flow and over another. Could also happen when moving over //TextLines that are either non-TLF or generated from a factory. var useTargetedTextLine:Boolean = false; if (target is TextLine) { var tfl:TextFlowLine = TextLine(target).userData as TextFlowLine; if (tfl) { var para:ParagraphElement = tfl.paragraph; if(para.getTextFlow() == textFlow) useTargetedTextLine = true; } } /* trace("got target class", target.toString(), "at (", localX, localY, ")"); trace("Mapping",localX,localY,"for",target); containerPoint = DisplayObject(target).localToGlobal(new Point(localX, localY)); trace("... Global",containerPoint.x,containerPoint.y); containerPoint = DisplayObject(currentTarget).globalToLocal(containerPoint); trace("... container Local",containerPoint.x,containerPoint.y); */ if (useTargetedTextLine) rslt = computeSelectionIndexInLine(textFlow, TextLine(target), localX, localY); else { var controller:ContainerController; for (var idx:int = 0; idx < textFlow.flowComposer.numControllers; idx++) { var testController:ContainerController = textFlow.flowComposer.getControllerAt(idx); if (testController.container == target || testController.container == currentTarget) { controller = testController; break; } } if (controller) { if (target != controller.container) { containerPoint = DisplayObject(target).localToGlobal(new Point(localX, localY)); containerPoint = DisplayObject(controller.container).globalToLocal(containerPoint); localX = containerPoint.x; localY = containerPoint.y; } rslt = computeSelectionIndexInContainer(textFlow, controller, localX, localY); } else { //the point is someplace else on stage. Map the target //to the textFlow.container. CONFIG::debug { assert(textFlow.flowComposer && textFlow.flowComposer.numControllers,"computeSelectionIndex: invalid textFlow"); } // result of the search var controllerCandidate:ContainerController = null; var candidateLocalX:Number; var candidateLocalY:Number; var relDistance:Number = Number.MAX_VALUE; for (var containerIndex:int = 0; containerIndex < textFlow.flowComposer.numControllers; containerIndex++) { var curContainerController:ContainerController = textFlow.flowComposer.getControllerAt(containerIndex); // displayed?? if (!checkForDisplayed(curContainerController.container as DisplayObject)) continue; // handle measured containers?? var bounds:Rectangle = curContainerController.getContentBounds(); var containerWidth:Number = isNaN(curContainerController.compositionWidth) ? curContainerController.effectivePaddingLeft+bounds.width : curContainerController.compositionWidth; var containerHeight:Number = isNaN(curContainerController.compositionHeight) ? curContainerController.effectivePaddingTop+bounds.height : curContainerController.compositionHeight; containerPoint = DisplayObject(target).localToGlobal(new Point(localX, localY)); containerPoint = DisplayObject(curContainerController.container).globalToLocal(containerPoint); // remove scrollRect effects for the distance test but add it back in for the result var adjustX:Number = 0; var adjustY:Number = 0; if (curContainerController.hasScrollRect) { containerPoint.x -= (adjustX = curContainerController.container.scrollRect.x); containerPoint.y -= (adjustY = curContainerController.container.scrollRect.y); } if ((containerPoint.x >= 0) && (containerPoint.x <= containerWidth) && (containerPoint.y >= 0) && (containerPoint.y <= containerHeight)) { controllerCandidate = curContainerController; candidateLocalX = containerPoint.x+adjustX; candidateLocalY = containerPoint.y+adjustY; break; } // figure minimum distance of containerPoint to curContainerController - 8 cases var relDistanceX:Number = 0; var relDistanceY:Number = 0; if (containerPoint.x < 0) { relDistanceX = containerPoint.x; if (containerPoint.y < 0) relDistanceY = containerPoint.y; else if (containerPoint.y > containerHeight) relDistanceY = containerPoint.y-containerHeight; } else if (containerPoint.x > containerWidth) { relDistanceX = containerPoint.x-containerWidth; if (containerPoint.y < 0) relDistanceY = containerPoint.y; else if (containerPoint.y > containerHeight) relDistanceY = containerPoint.y-containerHeight; } else if (containerPoint.y < 0) relDistanceY = -containerPoint.y; else relDistanceY = containerPoint.y-containerHeight; var tempDist:Number = relDistanceX*relDistanceX + relDistanceY*relDistanceY; // could do sqrt but why bother - there is no Math.hypot function if (tempDist <= relDistance) { relDistance = tempDist; controllerCandidate = curContainerController; candidateLocalX = containerPoint.x+adjustX; candidateLocalY = containerPoint.y+adjustY; } } rslt = controllerCandidate ? computeSelectionIndexInContainer(textFlow, controllerCandidate, candidateLocalX, candidateLocalY) : -1; } } if (rslt >= textFlow.textLength) rslt = textFlow.textLength-1; return rslt; } /** initialize a new point selection at click point @private */ tlf_internal function setNewSelectionPoint(currentTarget:Object, target:InteractiveObject, localX:Number, localY:Number, extendSelection:Boolean = false):Boolean { var selState:SelectionState = selectionPoint(currentTarget, target, localX, localY, extendSelection); if (selState == null) return false; // ignore if (selState.anchorPosition != anchorMark.position || selState.activePosition != activeMark.position) { // clear(false); // internalSetSelection(_textFlow, selState.anchorPosition, selState.activePosition); selectRange(selState.anchorPosition, selState.activePosition); return true; } return false; } // /////////////////////////////////// // Mouse and keyboard methods // /////////////////////////////////// /** * @copy IInteractionEventHandler#mouseDownHandler() * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function mouseDownHandler(event:MouseEvent):void { handleMouseEventForSelection(event, event.shiftKey); } /** * @copy IInteractionEventHandler#mouseMoveHandler() * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function mouseMoveHandler(event:MouseEvent):void { var wmode:String = textFlow.computedFormat.blockProgression; if (wmode != BlockProgression.RL) Mouse.cursor = MouseCursor.IBEAM; if (event.buttonDown) handleMouseEventForSelection(event, true); } private function handleMouseEventForSelection(event:MouseEvent, allowExtend:Boolean):void { var startSelectionActive:Boolean = hasSelection(); if (setNewSelectionPoint(event.currentTarget, event.target as InteractiveObject, event.localX, event.localY, startSelectionActive && allowExtend)) { selectionChanged(); if (startSelectionActive) clearSelectionShapes(); if (hasSelection()) addSelectionShapes(); } allowOperationMerge = false; } /** * @copy IInteractionEventHandler#mouseUpHandler() * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function mouseUpHandler(event:MouseEvent):void { if (!_mouseOverSelectionArea) { Mouse.cursor = MouseCursor.AUTO; } } private function atBeginningWordPos(activePara:ParagraphElement, pos:int):Boolean { if (pos == 0) return true; var nextPos:int = activePara.findNextWordBoundary(pos); nextPos = activePara.findPreviousWordBoundary(nextPos); return (pos == nextPos); } /** * @copy IInteractionEventHandler#mouseDoubleClickHandler() * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function mouseDoubleClickHandler(event:MouseEvent):void { if (!hasSelection()) return; // We got a previous single click event that set the selection. // Extend it into a selection for the entire word. // Adjust the active end to the beginning or end of the nearest word, depending on which end of the selection is active var activePara:ParagraphElement = _textFlow.findAbsoluteParagraph(activeMark.position); var activeParaStart:int = activePara.getAbsoluteStart(); var newActiveIndex:int; // adjusted active index if (anchorMark.position <= activeMark.position) newActiveIndex = activePara.findNextWordBoundary(activeMark.position - activeParaStart) + activeParaStart; else newActiveIndex = activePara.findPreviousWordBoundary(activeMark.position - activeParaStart) + activeParaStart; // don't include end of paragraph marker if (newActiveIndex == activeParaStart+activePara.textLength) newActiveIndex--; // Adjust the anchor end. If we're doing a dbl-click shift select to extend the selection, the anchor point stays the same. // Otherwise adjust it to the beginning or end of the nearest word, depending on which end of the selection is active var newAnchorIndex:int; // adjusted anchor index if (event.shiftKey) newAnchorIndex = anchorMark.position; else { var anchorPara:ParagraphElement = _textFlow.findAbsoluteParagraph(anchorMark.position); var anchorParaStart:int = anchorPara.getAbsoluteStart(); if (atBeginningWordPos(anchorPara, anchorMark.position - anchorParaStart)) { newAnchorIndex = anchorMark.position; } else { if (anchorMark.position <= activeMark.position) newAnchorIndex = anchorPara.findPreviousWordBoundary(anchorMark.position - anchorParaStart) + anchorParaStart; else newAnchorIndex = anchorPara.findNextWordBoundary(anchorMark.position - anchorParaStart) + anchorParaStart; // don't include end of paragraph marker if (newAnchorIndex == anchorParaStart+anchorPara.textLength) newAnchorIndex--; } } if (newAnchorIndex != anchorMark.position || newActiveIndex != activeMark.position) { internalSetSelection(_textFlow, newAnchorIndex, newActiveIndex, null); selectionChanged(); clearSelectionShapes(); if (hasSelection()) addSelectionShapes(); } allowOperationMerge = false; } /** * @copy IInteractionEventHandler#mouseOverHandler() * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function mouseOverHandler(event:MouseEvent):void { _mouseOverSelectionArea = true; var wmode:String = textFlow.computedFormat.blockProgression; if (wmode != BlockProgression.RL) Mouse.cursor = MouseCursor.IBEAM; else Mouse.cursor = MouseCursor.AUTO; } /** * @copy IInteractionEventHandler#mouseOutHandler() * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function mouseOutHandler(event:MouseEvent):void { _mouseOverSelectionArea = false; Mouse.cursor = MouseCursor.AUTO; } /** * @copy IInteractionEventHandler#focusInHandler() * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function focusInHandler(event:FocusEvent):void { // The focusIn can come before the activate. If so, we don't want the later activate to wipe out the focusIn _isActive = true; //trace("focusIn event on selectionManager", this.id); setSelectionFormatState(SelectionFormatState.FOCUSED); } /** * @copy IInteractionEventHandler#focusOutHandler() * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function focusOutHandler(event:FocusEvent):void { //trace("focusOut event on selectionManager", this.id); if (_isActive) // don't do it if we aren't active setSelectionFormatState(SelectionFormatState.UNFOCUSED); } /** * @copy IInteractionEventHandler#activateHandler() * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function activateHandler(event:Event):void { //trace("activate selectionManager", id); // If there are multiple containers, the selection manager will get multiple activate & deactivate events, // one per container. We only want to respond to the first one, because otherwise a focus event that comes // in the middle will get its state change overwritten. if (!_isActive) { _isActive = true; setSelectionFormatState(SelectionFormatState.UNFOCUSED); } } /** * @copy IInteractionEventHandler#deactivateHandler() * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function deactivateHandler(event:Event):void { //trace("deactivate selectionManager", id); // If there are multiple containers, the selection manager will get multiple activate & deactivate events, // one per container. We only want to respond to the first one, because otherwise a focus event that comes // in the middle will get its state change overwritten. if (_isActive) { _isActive = false; setSelectionFormatState(SelectionFormatState.INACTIVE); } } /** Perform a SelectionManager operation - these may never modify the flow but clients still are able to cancel them. * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function doOperation(op:FlowOperation):void { var opEvent:FlowOperationEvent = new FlowOperationEvent(FlowOperationEvent.FLOW_OPERATION_BEGIN,false,true,op,0,null); textFlow.dispatchEvent(opEvent); if (!opEvent.isDefaultPrevented()) { op = opEvent.operation; // only copy operation is allowed if (!(op is CopyOperation)) throw new IllegalOperationError(GlobalSettings.resourceStringFunction("illegalOperation",[ getQualifiedClassName(op) ])); var opError:Error = null; try { op.doOperation(); } catch(e:Error) { opError = e; } // operation completed - send event whether it succeeded or not. opEvent = new FlowOperationEvent(FlowOperationEvent.FLOW_OPERATION_END,false,true,op,0,opError); textFlow.dispatchEvent(opEvent); opError = opEvent.isDefaultPrevented() ? null : opEvent.error; if (opError) throw (opError); textFlow.dispatchEvent(new FlowOperationEvent(FlowOperationEvent.FLOW_OPERATION_COMPLETE,false,false,op,0,null)); } } /** * @copy IInteractionEventHandler#editHandler() * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function editHandler(event:Event):void { switch (event.type) { case Event.COPY: flushPendingOperations(); doOperation(new CopyOperation(getSelectionState())); break; case Event.SELECT_ALL: flushPendingOperations(); selectAll(); refreshSelection(); break; } } private function handleLeftArrow(event:KeyboardEvent):SelectionState { var selState:SelectionState = getSelectionState(); if(_textFlow.computedFormat.blockProgression != BlockProgression.RL) { if(_textFlow.computedFormat.direction == Direction.LTR) { if (event.ctrlKey || event.altKey) NavigationUtil.previousWord(selState,event.shiftKey); else NavigationUtil.previousCharacter(selState,event.shiftKey); } else { if (event.ctrlKey || event.altKey) NavigationUtil.nextWord(selState,event.shiftKey); else NavigationUtil.nextCharacter(selState,event.shiftKey); } } else { // always test for altkey first - that way ctrl-alt is the same as alt if (event.altKey) NavigationUtil.endOfParagraph(selState,event.shiftKey); else if (event.ctrlKey) NavigationUtil.endOfDocument(selState,event.shiftKey); else NavigationUtil.nextLine(selState,event.shiftKey); } return selState; } private function handleUpArrow(event:KeyboardEvent):SelectionState { var selState:SelectionState = getSelectionState(); if(_textFlow.computedFormat.blockProgression != BlockProgression.RL) { // always test for altkey first - that way ctrl-alt is the same as alt if (event.altKey) NavigationUtil.startOfParagraph(selState,event.shiftKey); else if (event.ctrlKey) NavigationUtil.startOfDocument(selState,event.shiftKey); else NavigationUtil.previousLine(selState,event.shiftKey); } else { if(_textFlow.computedFormat.direction == Direction.LTR) { if (event.ctrlKey || event.altKey) NavigationUtil.previousWord(selState,event.shiftKey); else NavigationUtil.previousCharacter(selState,event.shiftKey); } else { if (event.ctrlKey || event.altKey) NavigationUtil.nextWord(selState,event.shiftKey); else NavigationUtil.nextCharacter(selState,event.shiftKey); } } return selState; } private function handleRightArrow(event:KeyboardEvent):SelectionState { var selState:SelectionState = getSelectionState(); if(_textFlow.computedFormat.blockProgression != BlockProgression.RL) { if(_textFlow.computedFormat.direction == Direction.LTR) { if (event.ctrlKey || event.altKey) NavigationUtil.nextWord(selState,event.shiftKey); else NavigationUtil.nextCharacter(selState,event.shiftKey); } else { if (event.ctrlKey || event.altKey) NavigationUtil.previousWord(selState,event.shiftKey); else NavigationUtil.previousCharacter(selState,event.shiftKey); } } else { // always test for altkey first - that way ctrl-alt is the same as alt if (event.altKey) NavigationUtil.startOfParagraph(selState,event.shiftKey); else if (event.ctrlKey) NavigationUtil.startOfDocument(selState,event.shiftKey); else NavigationUtil.previousLine(selState,event.shiftKey); } return selState; } private function handleDownArrow(event:KeyboardEvent):SelectionState { var selState:SelectionState = getSelectionState(); if(_textFlow.computedFormat.blockProgression != BlockProgression.RL) { // always test for altkey first - that way ctrl-alt is the same as alt if (event.altKey) NavigationUtil.endOfParagraph(selState,event.shiftKey); else if (event.ctrlKey) NavigationUtil.endOfDocument(selState,event.shiftKey); else NavigationUtil.nextLine(selState,event.shiftKey); } else { if(_textFlow.computedFormat.direction == Direction.LTR) { if (event.ctrlKey || event.altKey) NavigationUtil.nextWord(selState,event.shiftKey); else NavigationUtil.nextCharacter(selState,event.shiftKey); } else { if (event.ctrlKey || event.altKey) NavigationUtil.previousWord(selState,event.shiftKey); else NavigationUtil.previousCharacter(selState,event.shiftKey); } } return selState; } private function handleHomeKey(event:KeyboardEvent):SelectionState { var selState:SelectionState = getSelectionState(); if (event.ctrlKey && !event.altKey) NavigationUtil.startOfDocument(selState,event.shiftKey); else NavigationUtil.startOfLine(selState,event.shiftKey); return selState; } private function handleEndKey(event:KeyboardEvent):SelectionState { var selState:SelectionState = getSelectionState(); if (event.ctrlKey && !event.altKey) NavigationUtil.endOfDocument(selState,event.shiftKey); else NavigationUtil.endOfLine(selState,event.shiftKey); return selState; } private function handlePageUpKey(event:KeyboardEvent):SelectionState { var selState:SelectionState = getSelectionState(); NavigationUtil.previousPage(selState,event.shiftKey); return selState; } private function handlePageDownKey(event:KeyboardEvent):SelectionState { var selState:SelectionState = getSelectionState(); NavigationUtil.nextPage(selState,event.shiftKey); return selState; } private function handleKeyEvent(event:KeyboardEvent):void { var selState:SelectionState = null; flushPendingOperations(); switch(event.keyCode) { case Keyboard.LEFT: selState = handleLeftArrow(event); break; case Keyboard.UP: selState = handleUpArrow(event); break; case Keyboard.RIGHT: selState = handleRightArrow(event); break; case Keyboard.DOWN: selState = handleDownArrow(event); break; case Keyboard.HOME: selState = handleHomeKey(event); break; case Keyboard.END: selState = handleEndKey(event); break; case Keyboard.PAGE_DOWN: selState = handlePageDownKey(event); break; case Keyboard.PAGE_UP: selState = handlePageUpKey(event); break; } if (selState != null) { event.preventDefault(); updateSelectionAndShapes(_textFlow, selState.anchorPosition, selState.activePosition); // make sure the active end is visible in the container -- scroll if necessary if (_textFlow.flowComposer && _textFlow.flowComposer.numControllers != 0) _textFlow.flowComposer.getControllerAt(_textFlow.flowComposer.numControllers-1).scrollToRange(selState.activePosition,selState.activePosition); } allowOperationMerge = false; } /** * @copy IInteractionEventHandler#keyDownHandler() * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function keyDownHandler(event:KeyboardEvent):void { if (!hasSelection() || event.isDefaultPrevented()) return; if (event.charCode == 0) { // the keycodes that we currently handle switch(event.keyCode) { case Keyboard.LEFT: case Keyboard.UP: case Keyboard.RIGHT: case Keyboard.DOWN: case Keyboard.HOME: case Keyboard.END: case Keyboard.PAGE_DOWN: case Keyboard.PAGE_UP: case Keyboard.ESCAPE: handleKeyEvent(event); break; } } else if (event.keyCode == Keyboard.ESCAPE) handleKeyEvent(event); } /** * @copy IInteractionEventHandler#keyUpHandler() * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 * @param event the keyUp event */ public function keyUpHandler(event:KeyboardEvent):void { //do nothing here } /** * @copy IInteractionEventHandler#keyFocusChangeHandler() * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 * @param event the FocusChange event */ public function keyFocusChangeHandler(event:FocusEvent):void { return; // ignores manageTabKey if not editable } /** * @copy IInteractionEventHandler#textInputHandler() * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function textInputHandler(event:TextEvent):void { // do nothing ignoreNextTextEvent = false; } /** * @copy IInteractionEventHandler#imeStartCompositionHandler() * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function imeStartCompositionHandler(event:IMEEvent):void { // Do nothing -- this is handled in the EditManager if editing is supported // If there is no EditManager, doing nothing will refuse the IME session. } /** * @private * * Execute asynchronous operations at the beginning of a frame. This * event listener is called only if there is work that needs to be done. */ protected function enterFrameHandler(event:Event):void { flushPendingOperations(); } /** * @copy IInteractionEventHandler#focusChangeHandler() */ public function focusChangeHandler(event:FocusEvent):void { } /** * @copy IInteractionEventHandler#menuSelectHandler() */ public function menuSelectHandler(event:ContextMenuEvent):void { var menu:ContextMenu = event.target as ContextMenu; if (activePosition != anchorPosition) { menu.clipboardItems.copy = true; menu.clipboardItems.cut = editingMode == EditingMode.READ_WRITE; menu.clipboardItems.clear = editingMode == EditingMode.READ_WRITE; } else { menu.clipboardItems.copy = false; menu.clipboardItems.cut = false; menu.clipboardItems.clear = false; } var systemClipboard:Clipboard = Clipboard.generalClipboard; if (activePosition != -1 && editingMode == EditingMode.READ_WRITE && (systemClipboard.hasFormat(TextClipboard.TEXT_LAYOUT_MARKUP) || systemClipboard.hasFormat(ClipboardFormats.TEXT_FORMAT))) { menu.clipboardItems.paste = true; } else { menu.clipboardItems.paste = false; } menu.clipboardItems.selectAll = true; } /** * @copy IInteractionEventHandler#mouseWheelHandler() */ public function mouseWheelHandler(event:MouseEvent):void { } /** * @copy IInteractionEventHandler#flushPendingOperations() * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function flushPendingOperations():void { } /** * @copy ISelectionManager#getCommonCharacterFormat() * * @includeExample examples\SelectionManager_getCommonCharacterFormat.as -noswf * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function getCommonCharacterFormat(range:TextRange=null):ITextLayoutFormat { if (!range && !hasSelection()) return null; var selRange:ElementRange = ElementRange.createElementRange(_textFlow, range ? range.absoluteStart : absoluteStart, range? range.absoluteEnd : absoluteEnd); var leaf:FlowLeafElement = selRange.firstLeaf; var attr:TextLayoutFormat = new TextLayoutFormat(leaf.computedFormat); // include any attributes set on a point selection but not yet applied and return if (!isRangeSelection()) { if (pointFormat) attr.apply(pointFormat) } else { for (;;) { if (leaf == selRange.lastLeaf) break; leaf = leaf.getNextLeaf(); attr.removeClashing(leaf.computedFormat); } } return Property.extractInCategory(TextLayoutFormat, TextLayoutFormat.description, attr, Category.CHARACTER) as ITextLayoutFormat; } /** * @copy ISelectionManager#getCommonParagraphFormat() * * @includeExample examples\SelectionManager_getCommonParagraphFormat.as -noswf * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function getCommonParagraphFormat (range:TextRange=null):ITextLayoutFormat { if (!range && !hasSelection()) return null; var selRange:ElementRange = ElementRange.createElementRange(_textFlow, range ? range.absoluteStart : absoluteStart, range? range.absoluteEnd : absoluteEnd); var para:ParagraphElement = selRange.firstParagraph; var attr:TextLayoutFormat = new TextLayoutFormat(para.computedFormat); for (;;) { if (para == selRange.lastParagraph) break; para = _textFlow.findAbsoluteParagraph(para.getAbsoluteStart()+para.textLength); attr.removeClashing(para.computedFormat); } return Property.extractInCategory(TextLayoutFormat,TextLayoutFormat.description,attr,Category.PARAGRAPH) as ITextLayoutFormat; } /** * @copy ISelectionManager#getCommonContainerFormat() * * @includeExample examples\SelectionManager_getCommonContainerFormat.as -noswf * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function getCommonContainerFormat (range:TextRange=null):ITextLayoutFormat { if (!range && !hasSelection()) return null; // TODO: FIX THIS - tlf_core supports mulutiple containers // container attributes changes through the UI apply to TextFlow and all containers // as of changelist# 625596, so all attributes values are 'common'. // note: that's true of your UI but that's no guarantee of common - the API supports users doing direct modifications outside the UI // need to revisit this when there are linked controllers CONFIG::debug { assert(textFlow.flowComposer.numControllers == 1,"multiple containers not supported by SelectionManager.getCommonContainerFormat"); } return Property.extractInCategory(TextLayoutFormat,TextLayoutFormat.description,textFlow.flowComposer.getControllerAt(0).computedFormat,Category.CONTAINER) as ITextLayoutFormat; } /** * Refreshes and displays TextFlow selection defined by a beginning and ending index. * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ private function updateSelectionAndShapes(tf:TextFlow, begIdx:int, endIdx:int):void { internalSetSelection(tf, begIdx, endIdx); if (_textFlow.flowComposer && _textFlow.flowComposer.numControllers != 0) _textFlow.flowComposer.getControllerAt(_textFlow.flowComposer.numControllers-1).scrollToRange(activeMark.position,anchorMark.position); selectionChanged(); clearSelectionShapes(); addSelectionShapes(); } /** @private */ CONFIG::debug tlf_internal function debugCheckTextFlow():int { if (flashx.textLayout.debug.Debugging.debugOn) return _textFlow.debugCheckTextFlow(); return 0; } private var marks:Array = []; /** @private */ tlf_internal function createMark():Mark { var mark:Mark = new Mark(-1); marks.push(mark); return mark; } /** @private */ tlf_internal function removeMark(mark:Mark):void { var idx:int = marks.indexOf(mark); if (idx != -1) marks.splice(idx,idx+1); } /** * @copy ISelectionManager#notifyInsertOrDelete() * * @includeExample examples\SelectionManager_notifyInsertOrDelete.as -noswf * * @playerversion Flash 10 * @playerversion AIR 1.5 * @langversion 3.0 */ public function notifyInsertOrDelete(absolutePosition:int, length:int):void { if (length == 0) return; for (var i:int = 0; i < marks.length; i++) { var mark:Mark = marks[i]; if (mark.position >= absolutePosition) { if (length < 0) mark.position = (mark.position + length < absolutePosition) ? absolutePosition : mark.position + length; else mark.position += length; } } } } }