////////////////////////////////////////////////////////////////////////////////
//
// ADOBE SYSTEMS INCORPORATED
// Copyright 2007-2010 Adobe Systems Incorporated
// All Rights Reserved.
//
// NOTICE:  Adobe permits you to use, modify, and distribute this file 
// in accordance with the terms of the license agreement accompanying it.
//
////////////////////////////////////////////////////////////////////////////////
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.IFlowComposer;
    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.
     * 
     * <p>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 <code>interactionManager</code>
     * property of the flow. (To allow editing, assign an instance of the EditManager class,
     * which extends SelectionManager.)</p>
     *
     * <p>The following table describes how the SelectionManager class handles keyboard shortcuts:</p>
     *
     * <table class="innertable" width="100%">
     * <thead>
     * <tr><th></th><th></th><th align = "center">TB,LTR</th><th align = "right"></th><th></th><th align = "center">TB,RTL</th><th></th><th></th><th align = "center">TL,LTR</th><th></th><th></th><th align = "center">RL,RTL</th><th></th></tr>
     * <tr><th></th><th>none</th><th>ctrl</th><th>alt|ctrl+alt</th><th>none</th><th>ctrl</th><th>alt|ctrl+alt</th><th>none</th><th>ctrl</th><th>alt|ctrl+alt</th><th>none</th><th>ctrl</th><th>alt|ctrl+alt</th></tr>
     * </thead>
     * <tr><td>leftarrow</td><td>previousCharacter</td><td>previousWord</td><td>previousWord</td><td>nextCharacter</td><td>nextWord</td><td>nextWord</td><td>nextLine</td><td>endOfDocument</td><td>endOfParagraph</td><td>nextLine</td><td>endOfDocument</td><td>endOfParagraph</td></tr>
     * <tr><td>uparrow</td><td>previousLine</td><td>startOfDocument</td><td>startOfParagraph</td><td>previousLine</td><td>startOfDocument</td><td>startOfParagraph</td><td>previousCharacter</td><td>previousWord</td><td>previousWord</td><td>nextCharacter</td><td>nextWord</td><td>nextWord</td></tr>
     * <tr><td>rightarrow</td><td>nextCharacter</td><td>nextWord</td><td>nextWord</td><td>previousCharacter</td><td>previousWord</td><td>previousWord</td><td>previousLine</td><td>startOfDocument</td><td>startOfParagraph</td><td>previousLine</td><td>startOfDocument</td><td>startOfParagraph</td></tr>
     * <tr><td>downarrow</td><td>nextLine</td><td>endOfDocument</td><td>endOfParagraph</td><td>nextLine</td><td>endOfDocument</td><td>endOfParagraph</td><td>nextCharacter</td><td>nextWord</td><td>nextWord</td><td>previousCharacter</td><td>previousWord</td><td>previousWord</td></tr>
     * <tr><td>home</td><td>startOfLine</td><td>startOfDocument</td><td>startOfLine</td><td>startOfLine</td><td>startOfDocument</td><td>startOfLine</td><td>startOfLine</td><td>startOfDocument</td><td>startOfLine</td><td>startOfLine</td><td>startOfDocument</td><td>startOfLine</td></tr>
     * <tr><td>end</td><td>endOfLine</td><td>endOfDocument</td><td>endOfLine</td><td>endOfLine</td><td>endOfDocument</td><td>endOfLine</td><td>endOfLine</td><td>endOfDocument</td><td>endOfLine</td><td>endOfLine</td><td>endOfDocument</td><td>endOfLine</td></tr>
     * <tr><td>pagedown</td><td>nextPage</td><td>nextPage</td><td>nextPage</td><td>nextPage</td><td>nextPage</td><td>nextPage</td><td>nextPage</td><td>nextPage</td><td>nextPage</td><td>nextPage</td><td>nextPage</td><td>nextPage</td></tr>
     * <tr><td>pageup</td><td>previousPage</td><td>previousPage</td><td>previousPage</td><td>previousPage</td><td>previousPage</td><td>previousPage</td><td>previousPage</td><td>previousPage</td><td>previousPage</td><td>previousPage</td><td>previousPage</td><td>previousPage</td></tr>
     * </table>
     *
     * <p><strong>Key:</strong>
     * <ul>
     *  <li>none = no modifier</li>
     *  <li>ctrl, shift, alt = modifiers</li>
     *  <li>alt-key and ctrl+alt-key are the same on all platforms (on some platforms alt-key does not get to the Text Layout Framework (TLF)</li>
     *  <li>shift key modifes to extend the active end of the selection in the specified manner</li>            
     *  <li>TB (top-to-bottom),RL (right-to-left) are textFlow level <code>blockProgression</code> settings</li>                        
     *  <li>LTR (left-to-right),RTL (right-to-left) are textFlow level <code>direction</code> settings</li>                 
     *  <li>next and prev in logical order in the textFlow - the effect in RTL text is that the selection moves in the physical direction</li>
     * </ul></p>
     * 
     * @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.
         * 
         * <p>Assign a SelectionManager object to the <code>interactionManager</code> property of
         * a text flow to enable text selection.</p>
         * 
         * @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. 
         * 
         * <p>A selection manager manages a single text flow. A selection manager can also be
         * assigned to a text flow by setting the <code>interactionManager</code> property of the
         * TextFlow object.</p>
         * 
         * @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();
                
                // If we switch into read-only mode, make sure the cursor isn't showing a text selection IBeam
                if (!value) // see Watson 2637162
                    setMouseCursor(MouseCursor.AUTO);
        
                _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
                        
            // make sure we aren't selecting after the flow terminating character
            endIdx = Math.min(endIdx, _textFlow.textLength - 1);
            
            if (!extendSelection)
                begIdx = endIdx;                            

            if (begIdx == 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 (!_textFlow)
                 return;
             
            //  trace("setFocus sm", id);

            // container with the activePosition gets the key focus
            if (_textFlow.flowComposer)
                _textFlow.flowComposer.setFocus(activePosition,false);
            setSelectionFormatState(SelectionFormatState.FOCUSED);
         }
         
        /** 
         * Set the Mouse cursor. 
         * @param cursor    New cursor value, MouseCursor.AUTO if mouse is going off text, MouseCursor.IBEAM if mouse is going into text
         * 
         * @playerversion Flash 10.2
         * @playerversion AIR 2.0
         * @langversion 3.0
         */
        protected function setMouseCursor(cursor:String):void
        {
            Mouse.cursor = cursor;
        }
        
        /**
         *  @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;
            }
            
            var lastSelectablePos:int = (_textFlow.textLength > 0) ? _textFlow.textLength - 1 : 0;
            
            if (anchorPosition != -1 && activePosition != -1)
            {
                if (anchorPosition > lastSelectablePos)
                    anchorPosition = lastSelectablePos;
                
                if (activePosition > lastSelectablePos)
                    activePosition = lastSelectablePos;
            }

            _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
        {
            if (_textFlow.flowComposer)
            {
                // selection may need to be constrainted due to model changes that weren't done with the EditManager
                internalSetSelection(_textFlow,anchorMark.position,activeMark.position,_pointFormat);
                
                // 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
        {
            var flowComposer:IFlowComposer = _textFlow ? _textFlow.flowComposer : null; 
            if (flowComposer)
            {
                var containerIter:int = 0;
                while(containerIter < flowComposer.numControllers)
                {
                    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
                var lastPosition:int = _textFlow.textLength > 0 ? _textFlow.textLength - 1 : 0;
                rslt += assert(anchorMark.position >= 0 && anchorMark.position <= lastPosition,"SelectionManager:validate selBegIdx is out of range");
                rslt += assert(activeMark.position >= 0 && activeMark.position <= lastPosition,"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)
                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 textFlowLine:TextFlowLine = textFlow.flowComposer.getLineAt(lineIndex);
            var textLine:TextLine = textFlowLine.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 startOnNextLineIfNecessary:Boolean = false;
            
            var lastAtom:int = -1;
            if (isDirectionRTL) {
                lastAtom = textLine.atomCount - 1;
            } else {
                if ((textFlowLine.absoluteStart + textFlowLine.textLength) >= textFlowLine.paragraph.getAbsoluteStart() + textFlowLine.paragraph.textLength) {
                    if (textLine.atomCount > 1) lastAtom = 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) lastAtom = textLine.atomCount - 2;
                    } else {
                        startOnNextLineIfNecessary = true;
                        if (textLine.atomCount > 0) lastAtom = textLine.atomCount - 1;
                    }
                }
            }
            var lastAtomRect:Rectangle = (lastAtom > 0) ? textLine.getAtomBounds(lastAtom) : new Rectangle(0, 0, 0, 0);
                        
            if (!isTTB)
            {
                if (localX < 0)
                    localX = 0;
                else if (localX > (lastAtomRect.x + lastAtomRect.width))
                {
                    if (startOnNextLineIfNecessary) 
                        return textFlowLine.absoluteStart + textFlowLine.textLength - 1;
                    if (lastAtomRect.x + lastAtomRect.width > 0)
                        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 - 1;    
                    if (lastAtomRect.y + lastAtomRect.height > 0)
                        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 (textLine.parent)
                    pt = textLine.parent.globalToLocal(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.getTotalPaddingLeft()+bounds.width : curContainerController.compositionWidth;
                        var containerHeight:Number = isNaN(curContainerController.compositionHeight) ? curContainerController.getTotalPaddingTop()+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) 
                setMouseCursor(MouseCursor.IBEAM);          
            if (event.buttonDown)
                handleMouseEventForSelection(event, true);
        }
        
        /** @private */
        tlf_internal 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))
            {
                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)
            {
                setMouseCursor(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) 
                setMouseCursor(MouseCursor.IBEAM);  
            else 
                setMouseCursor(MouseCursor.AUTO);                               
        }

        /** 
         * @copy IInteractionEventHandler#mouseOutHandler()
         * 
         * @playerversion Flash 10
         * @playerversion AIR 1.5
         * @langversion 3.0
         */                 
        public function mouseOutHandler(event:MouseEvent):void
        {
            _mouseOverSelectionArea = false;            
            setMouseCursor(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.
        }
        
        /** 
         * @copy IInteractionEventHandler#softKeyboardActivatingHandler()
         * 
         * @playerversion Flash 10.2
         * @playerversion AIR 1.5
         * @langversion 3.0
         */
        public function softKeyboardActivatingHandler(event:Event):void
        {
            // Do nothing -- this is handled in the EditManager if editing is supported
        }
        
        /**
         *  @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):TextLayoutFormat
        {
            if (!range && !hasSelection())
                return null;
            
            var selRange:ElementRange = ElementRange.createElementRange(_textFlow, range ? range.absoluteStart : absoluteStart, range? range.absoluteEnd : absoluteEnd);
            var rslt:TextLayoutFormat = selRange.getCommonCharacterFormat();
                
            // include any attributes set on a point selection but not yet applied  
            if (selRange.absoluteEnd == selRange.absoluteStart && pointFormat)
                rslt.apply(pointFormat)
    
            return rslt;
        }
        
         
         /**
         * @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):TextLayoutFormat
        {
            if (!range && !hasSelection())
                return null;
            
            return ElementRange.createElementRange(_textFlow, range ? range.absoluteStart : absoluteStart, range? range.absoluteEnd : absoluteEnd).getCommonParagraphFormat();
         }
         
        /**
         * @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):TextLayoutFormat
        {
            if (!range && !hasSelection())
                return null;
            
            return ElementRange.createElementRange(_textFlow, range ? range.absoluteStart : absoluteStart, range? range.absoluteEnd : absoluteEnd).getCommonContainerFormat();
        }
         
        /**
         * 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;
                }
            }
        }
    }
}
