1////////////////////////////////////////////////////////////////////////////////
2//
3//  ADOBE SYSTEMS INCORPORATED
4//  Copyright 2008 Adobe Systems Incorporated
5//  All Rights Reserved.
6//
7//  NOTICE: Adobe permits you to use, modify, and distribute this file
8//  in accordance with the terms of the license agreement accompanying it.
9//
10////////////////////////////////////////////////////////////////////////////////
11
12package spark.components.supportClasses
13{
14
15import flash.display.BlendMode;
16import flash.display.Graphics;
17import flash.events.Event;
18import flash.events.FocusEvent;
19import flash.events.KeyboardEvent;
20import flash.events.MouseEvent;
21import flash.geom.Rectangle;
22import flash.ui.ContextMenu;
23import flash.ui.Keyboard;
24
25import flashx.textLayout.container.ContainerController;
26import flashx.textLayout.container.TextContainerManager;
27import flashx.textLayout.edit.EditManager;
28import flashx.textLayout.edit.EditingMode;
29import flashx.textLayout.edit.ElementRange;
30import flashx.textLayout.edit.IEditManager;
31import flashx.textLayout.edit.ISelectionManager;
32import flashx.textLayout.edit.SelectionFormat;
33import flashx.textLayout.edit.SelectionManager;
34import flashx.textLayout.edit.SelectionState;
35import flashx.textLayout.elements.FlowLeafElement;
36import flashx.textLayout.elements.IConfiguration;
37import flashx.textLayout.elements.ParagraphElement;
38import flashx.textLayout.elements.TextFlow;
39import flashx.textLayout.elements.TextRange;
40import flashx.textLayout.events.SelectionEvent;
41import flashx.textLayout.formats.Category;
42import flashx.textLayout.formats.ITextLayoutFormat;
43import flashx.textLayout.formats.TextLayoutFormat;
44import flashx.textLayout.operations.ApplyFormatOperation;
45import flashx.textLayout.operations.InsertTextOperation;
46import flashx.textLayout.property.Property;
47import flashx.textLayout.tlf_internal;
48import flashx.undo.IUndoManager;
49import flashx.undo.UndoManager;
50
51import mx.core.mx_internal;
52import mx.events.SandboxMouseEvent;
53import mx.styles.IStyleClient;
54
55import spark.components.RichEditableText;
56import spark.components.TextSelectionHighlighting;
57
58use namespace mx_internal;
59use namespace tlf_internal;
60
61[ExcludeClass]
62
63/**
64 *  @private
65 *  A subclass of TextContainerManager that manages the text in
66 *  a RichEditableText component.
67 *
68 *  @langversion 3.0
69 *  @playerversion Flash 10
70 *  @playerversion AIR 1.5
71 *  @productversion Flex 4
72 */
73public class RichEditableTextContainerManager extends TextContainerManager
74{
75    /**
76     *  Constructor.
77     *
78     *  @langversion 3.0
79     *  @playerversion Flash 10
80     *  @playerversion AIR 1.5
81     *  @productversion Flex 4
82     */
83    public function RichEditableTextContainerManager(
84                        container:RichEditableText,
85                        configuration:IConfiguration=null)
86    {
87        super(container, configuration);
88
89        textDisplay = container;
90    }
91
92    //--------------------------------------------------------------------------
93    //
94    //  Variables
95    //
96    //--------------------------------------------------------------------------
97
98    /**
99     *  @private
100     */
101    private var hasScrollRect:Boolean = false;
102
103    /**
104     *  @private
105     */
106    private var textDisplay:RichEditableText;
107
108    //--------------------------------------------------------------------------
109    //
110    //  Overridden methods
111    //
112    //--------------------------------------------------------------------------
113
114    /**
115     *  @private
116     */
117    override public function drawBackgroundAndSetScrollRect(
118                                    scrollX:Number, scrollY:Number):Boolean
119    {
120        // If not auto-sizing these are the same as the compositionWidth/Height.
121        // If auto-sizing, the compositionWidth/Height may be NaN.  If no
122        // constraints this will reflect the actual size of the text.
123        var width:Number = textDisplay.width;
124        var height:Number = textDisplay.height;
125
126        var contentBounds:Rectangle = getContentBounds();
127
128        // If measuring width, use the content width.
129        if (isNaN(width))
130            width = contentBounds.right;
131
132        // If measuring height, use the content height.
133        if (isNaN(height))
134            height = contentBounds.bottom;
135
136        // TODO:(cframpto)  Adjust for RL text.
137        // See ContainerController.updateVisibleRectangle().
138        // (effectiveBlockProgression == BlockProgression.RL) ? -width : 0;
139        var xOrigin:Number = 0;
140
141        // If autoSize, and lineBreak="toFit" there should never be
142        // a scroll rect but if lineBreak="explicit" the text may need
143        // to be clipped.
144        if (scrollX == 0 && scrollY == 0 &&
145            contentBounds.left >= xOrigin &&
146            contentBounds.right <= width &&
147            contentBounds.top >= 0 &&
148            contentBounds.bottom <= height)
149        {
150            // skip the scrollRect
151            if (hasScrollRect)
152            {
153                container.scrollRect = null;
154                hasScrollRect = false;
155            }
156        }
157        else
158        {
159            container.scrollRect = new Rectangle(scrollX, scrollY, width, height);
160            hasScrollRect = true;
161        }
162
163        // Client must draw a background to get mouse events,
164        // even it if is 100% transparent.
165    	// If backgroundColor is defined, fill the bounds of the component
166    	// with backgroundColor drawn with alpha level backgroundAlpha.
167    	// Otherwise, fill with transparent black.
168    	// (The color in this case is irrelevant.)
169    	var color:uint = 0x000000;
170    	var alpha:Number = 0.0;
171    	var styleableContainer:IStyleClient = container as IStyleClient;
172    	if (styleableContainer)
173    	{
174    		var backgroundColor:* =
175    			styleableContainer.getStyle("backgroundColor");
176    		if (backgroundColor !== undefined)
177    		{
178    			color = uint(backgroundColor);
179    			alpha = styleableContainer.getStyle("backgroundAlpha");
180    		}
181    	}
182        // TODO (cframpto):  Adjust for RL text.  See
183        // ContainerController.attachTransparentBackgroundForHit().
184        var g:Graphics = container.graphics;
185        g.clear();
186        g.lineStyle();
187        g.beginFill(color, alpha);
188        g.drawRect(scrollX, scrollY, width, height);
189        g.endFill();
190
191        return hasScrollRect;
192    }
193
194    /**
195     *  @private
196     *
197     * If the user specified a custom context menu then use
198     * it rather than the default context menu. It must be set before the
199     * first mouse over/mouse hover or foucsIn event to be used.
200     *
201     * TLF will remove the context menu when it switches from the factory
202     * to the composer and the controller will then request it again.
203     */
204    override tlf_internal function getContextMenu():ContextMenu
205    {
206        // ToDo(cframpto): Ideally could specify the context
207        // menu on the TextArea or the TextInput and it wouldn't be obscured
208        // by TLF's context menu.
209
210        // Return null to use the existing contextMenu on the container.
211        // Otherwise the TCM will overwrite this contextMenu.
212        return textDisplay.contextMenu != null ? null : super.getContextMenu();
213    }
214
215    /**
216     *  @private
217     */
218    override protected function getUndoManager():IUndoManager
219    {
220        if (!textDisplay.undoManager)
221        {
222            textDisplay.undoManager = new UndoManager();
223            textDisplay.undoManager.undoAndRedoItemLimit = int.MAX_VALUE;
224        }
225
226        return textDisplay.undoManager;
227    }
228
229    /**
230     *  @private
231     */
232    override protected function getFocusedSelectionFormat():SelectionFormat
233    {
234        var selectionColor:* = textDisplay.getStyle("focusedTextSelectionColor");
235
236        var focusedPointAlpha:Number =
237            editingMode == EditingMode.READ_WRITE ?
238            1.0 :
239            0.0;
240
241        // If editable, the insertion point is black, inverted, which makes it
242        // the inverse color of the background, for maximum readability.
243        // If not editable, then no insertion point.
244        return new SelectionFormat(
245            selectionColor, 1.0, BlendMode.NORMAL,
246            0x000000, focusedPointAlpha, BlendMode.INVERT);
247    }
248
249    /**
250     *  @private
251     */
252    override protected function getUnfocusedSelectionFormat():SelectionFormat
253    {
254        var unfocusedSelectionColor:* = textDisplay.getStyle(
255                                            "unfocusedTextSelectionColor");
256
257        var unfocusedAlpha:Number =
258            textDisplay.selectionHighlighting !=
259            TextSelectionHighlighting.WHEN_FOCUSED ?
260            1.0 :
261            0.0;
262
263        // No insertion point when no focus.
264        return new SelectionFormat(
265            unfocusedSelectionColor, unfocusedAlpha, BlendMode.NORMAL,
266            unfocusedSelectionColor, 0.0);
267    }
268
269    /**
270     *  @private
271     */
272    override protected function getInactiveSelectionFormat():SelectionFormat
273    {
274        var inactiveSelectionColor:* = textDisplay.getStyle(
275                                            "inactiveTextSelectionColor");
276
277        var inactiveAlpha:Number =
278            textDisplay.selectionHighlighting ==
279            TextSelectionHighlighting.ALWAYS ?
280            1.0 :
281            0.0;
282
283        // No insertion point when not active.
284        return new SelectionFormat(
285            inactiveSelectionColor, inactiveAlpha, BlendMode.NORMAL,
286            inactiveSelectionColor, 0.0);
287    }
288
289    /**
290     *  @private
291     */
292    override protected function createEditManager(
293                        undoManager:flashx.undo.IUndoManager):IEditManager
294    {
295        var editManager:IEditManager = super.createEditManager(undoManager);
296
297        // Default is to batch text input.  If the component, like ComboBox
298        // wants to act on each keystroke then set this to false.
299        editManager.allowDelayedOperations = textDisplay.batchTextInput;
300
301        // Do not delayUpdates until further work is done to ensure our public API methods to
302        // format and insert text work correctly.  TLF does not dispatch the selectionChange
303        // event until the display is updated which means our selection properties may not
304        // be in sync with the TLF values. This could matter for our API methods that
305        // take the selection as parameters or default to the current selection.  In the
306        // former case, the user could query for the selection or rely on the selectionChange
307        // event and get incorrect values if there is a pending update and in the later case
308        // we fill in the default selection which might not be current if there is a pending
309        // update.
310        editManager.delayUpdates = false;
311
312        return editManager;
313    }
314
315    /**
316     *  @private
317     */
318    override public function setText(text:String):void
319    {
320        super.setText(text);
321
322        // If we have focus, need to make sure we can still input text.
323        initForInputIfHaveFocus();
324    }
325
326    /**
327     *  @private
328     */
329    override public function setTextFlow(textFlow:TextFlow):void
330    {
331        super.setTextFlow(textFlow);
332
333        // If we have focus, need to make sure we can still input text.
334        initForInputIfHaveFocus();
335    }
336
337    /**
338     *  @private
339     */
340    private function initForInputIfHaveFocus():void
341    {
342        // If we have focus, need to make sure there is a composer in place,
343        // the new controller knows it has focus, and there is an insertion
344        // point so input works without a mouse over or mouse click.  Normally
345        // this is done in our focusIn handler by making sure there is a
346        // selection.  Test this by clicking an arrow in the NumericStepper
347        // and then entering a number without clicking on the input field first.
348        if (editingMode != EditingMode.READ_ONLY &&
349            textDisplay.getFocus() == textDisplay)
350        {
351            // this will ensure a text flow with a comopser
352            var im:ISelectionManager = beginInteraction();
353
354            var controller:ContainerController =
355                getTextFlow().flowComposer.getControllerAt(0);
356
357            controller.requiredFocusInHandler(null);
358
359            if (!textDisplay.preserveSelectionOnSetText)
360                im.selectRange(0, 0);
361
362            endInteraction();
363        }
364    }
365
366    /**
367     *  @private
368     *  To apply a format to a selection in a textFlow without using the
369     *  selection manager.
370     */
371    mx_internal function applyFormatOperation(
372                            leafFormat:ITextLayoutFormat,
373                            paragraphFormat:ITextLayoutFormat,
374                            containerFormat:ITextLayoutFormat,
375                            anchorPosition:int,
376                            activePosition:int):Boolean
377    {
378        // Nothing to do.
379        if (anchorPosition == -1 || activePosition == -1)
380            return true;
381
382        var textFlow:TextFlow = getTextFlowWithComposer();
383
384        var operationState:SelectionState =
385            new SelectionState(textFlow, anchorPosition, activePosition);
386
387        // If using the edit manager and the selection is the current selection,
388        // need to set the flag so point selection is set with pending formats for next
389        // char typed.
390        const editManager:IEditManager = textFlow.interactionManager as IEditManager;
391        if (editManager)
392        {
393            const absoluteStart:int = getAbsoluteStart(anchorPosition, activePosition);
394            const absoluteEnd:int = getAbsoluteEnd(anchorPosition, activePosition);
395
396            if (editManager.absoluteStart == absoluteStart && editManager.absoluteEnd == absoluteEnd)
397                operationState.selectionManagerOperationState = true;
398        }
399
400        // For the case when interactive editing is not allowed.
401        var op:ApplyFormatOperation =
402            new ApplyFormatOperation(
403                operationState, leafFormat, paragraphFormat, containerFormat);
404
405        var success:Boolean = op.doOperation();
406        if (success)
407        {
408            textFlow.normalize();
409            textFlow.flowComposer.updateAllControllers();
410        }
411
412        return success;
413    }
414
415    /**
416     *  @private
417     *  To get the format of a character.  Our API allows this operation even
418     *  when the editingMode does not permit either interactive selection or
419     *  editing.
420     */
421    mx_internal function getCommonCharacterFormat(
422                                        anchorPosition:int,
423                                        activePosition:int):ITextLayoutFormat
424    {
425        if (anchorPosition == -1 || activePosition == -1)
426            return null;
427
428        var textFlow:TextFlow = getTextFlowWithComposer();
429
430        if (textFlow.interactionManager)
431        {
432            // If there is a selection manager use it so that the format,
433            // depending on the range, may include any attributes set on a point
434            // selection but not yet applied.
435            const range:TextRange =
436                new TextRange(textFlow, anchorPosition, activePosition);
437
438            return textFlow.interactionManager.getCommonCharacterFormat(range);
439        }
440        else
441        {
442            // ElementRange will order the selection points.  Since there isn't
443            // an interactionManager there is not a point selection to worry
444            // about.
445            var selRange:ElementRange =
446                ElementRange.createElementRange(textFlow, anchorPosition, activePosition);
447
448            return selRange.getCommonCharacterFormat();
449        }
450    }
451
452    /**
453     *  @private
454     *  To get the format of the container without using a SelectionManager.
455     *  The method should be kept in sync with the version in the
456     *  SelectionManager.
457     */
458    mx_internal function getCommonContainerFormat():ITextLayoutFormat
459    {
460        var textFlow:TextFlow = getTextFlowWithComposer();
461
462        // absoluteStart and absoluteEnd values not used.
463        var selRange:ElementRange =
464            ElementRange.createElementRange(textFlow, 0, textFlow.textLength - 1);
465
466        return selRange.getCommonContainerFormat();
467    }
468
469    /**
470     *  @private
471     *  To get the format of a paragraph without using a SelectionManager.
472     *  The method should be kept in sync with the version in the
473     *  SelectionManager.
474     */
475    mx_internal function getCommonParagraphFormat(
476                                        anchorPosition:int,
477                                        activePosition:int):ITextLayoutFormat
478    {
479        if (anchorPosition == -1 || activePosition == -1)
480            return null;
481
482        var textFlow:TextFlow = getTextFlowWithComposer();
483
484        // ElementRange will order the selection points.
485        var selRange:ElementRange =
486            ElementRange.createElementRange(textFlow, anchorPosition, activePosition);
487
488        return selRange.getCommonParagraphFormat();
489    }
490
491    /**
492     *  @private
493     *  Insert or append text to the textFlow without using an EditManager.
494     *  If there is a SelectionManager or EditManager its selection will be
495     *  updated at the end of the operation to keep it in sync.
496     */
497    mx_internal function insertTextOperation(insertText:String,
498                                             anchorPosition:int,
499                                             activePosition:int):Boolean
500    {
501        // No insertion point.
502        if (anchorPosition == -1 || activePosition == -1)
503            return false;
504
505        var textFlow:TextFlow = getTextFlowWithComposer();
506
507        var absoluteStart:int = getAbsoluteStart(anchorPosition, activePosition);
508        var absoluteEnd:int = getAbsoluteEnd(anchorPosition, activePosition);
509
510        // Need to get the format of the insertion point so that the inserted
511        // text will have this format.
512        var pointFormat:ITextLayoutFormat =
513            getCommonCharacterFormat(absoluteStart, absoluteStart);
514
515        var operationState:SelectionState =
516            new SelectionState(textFlow, absoluteStart, absoluteEnd, pointFormat);
517
518        // If there is an interaction manager, this keeps it in sync with
519        // the results of this operation.
520        operationState.selectionManagerOperationState = true;
521
522        var op:InsertTextOperation =
523            new InsertTextOperation(operationState, insertText);
524
525        // Generations don't seem to be used in this code path since we
526        // aren't doing composite, merge or undo operations so they were
527        // optimized out.
528
529        var success:Boolean = op.doOperation();
530        if (success)
531        {
532            textFlow.normalize();
533
534            textFlow.flowComposer.updateAllControllers();
535
536            var insertPt:int = absoluteEnd - (absoluteEnd - absoluteStart) +
537                                    + insertText.length;
538
539            // No point format.
540            var selectionState:SelectionState =
541                new SelectionState(textFlow, insertPt, insertPt);
542
543            var selectionEvent:SelectionEvent =
544                new SelectionEvent(SelectionEvent.SELECTION_CHANGE,
545                                   false, false, selectionState);
546
547            textFlow.dispatchEvent(selectionEvent);
548
549            scrollToRange(insertPt, insertPt);
550        }
551
552        return success;
553    }
554
555    /**
556     *  Note:  It is probably a TLF bug that, if delayedUpdates is true, we have to call
557     *  updateAllControllers before doing a format operation to guarantee the correct
558     *  results.
559     */
560    mx_internal function getTextFlowWithComposer():TextFlow
561    {
562        var textFlow:TextFlow = getTextFlow();
563
564        // Make sure there is a text flow with a flow composer.  There will
565        // not be an interaction manager if editingMode is read-only.  If
566        // there is an interaction manager flush any pending inserts into the
567        // text flow unless we are delaying updates in which case we may have to finish
568        // composition.
569        if (composeState != TextContainerManager.COMPOSE_COMPOSER)
570        {
571            convertToTextFlowWithComposer();
572        }
573        else if (textFlow.interactionManager)
574        {
575            const editManager:IEditManager = textFlow.interactionManager as IEditManager;
576            if (editManager && editManager.delayUpdates)
577                editManager.updateAllControllers();
578            else
579                textFlow.interactionManager.flushPendingOperations();
580        }
581
582        return textFlow;
583    }
584
585    /**
586     *  @private
587     */
588    private function getAbsoluteStart(anchorPosition:int, activePosition:int):int
589    {
590        return (anchorPosition < activePosition) ?
591                    anchorPosition : activePosition;
592    }
593
594    /**
595     *  @private
596     */
597    private function getAbsoluteEnd(anchorPosition:int, activePosition:int):int
598    {
599        return (anchorPosition > activePosition) ?
600                    anchorPosition : activePosition;
601    }
602
603    //--------------------------------------------------------------------------
604    //
605    //  Overridden event handlers
606    //
607    //--------------------------------------------------------------------------
608
609    /**
610     *  @private
611     */
612    override public function focusInHandler(event:FocusEvent):void
613    {
614        textDisplay.focusInHandler(event);
615
616        super.focusInHandler(event);
617    }
618
619    /**
620     *  @private
621     */
622    override public function focusOutHandler(event:FocusEvent):void
623    {
624        textDisplay.focusOutHandler(event);
625
626        super.focusOutHandler(event);
627    }
628
629    /**
630     *  @private
631     */
632    override public function keyDownHandler(event:KeyboardEvent):void
633    {
634        textDisplay.keyDownHandler(event);
635
636        if (!event.isDefaultPrevented())
637        {
638            var clone:KeyboardEvent = KeyboardEvent(event.clone());
639            super.keyDownHandler(clone);
640            if (clone.isDefaultPrevented())
641                event.preventDefault();
642        }
643    }
644
645    /**
646     *  @private
647     */
648    override public function keyUpHandler(event:KeyboardEvent):void
649    {
650        if (!event.isDefaultPrevented())
651        {
652            var clone:KeyboardEvent = KeyboardEvent(event.clone());
653            super.keyUpHandler(clone);
654            if (clone.isDefaultPrevented())
655                event.preventDefault();
656        }
657    }
658
659    /**
660     *  @private
661     */
662    override public function mouseDownHandler(event:MouseEvent):void
663    {
664        textDisplay.mouseDownHandler(event);
665
666        super.mouseDownHandler(event);
667    }
668
669    /**
670     *  @private
671     *  This handler gets called for ACTIVATE events from the player
672     *  and FLEX_WINDOW_ACTIVATE events from Flex.  Because of the
673     *  way AIR handles activation of AIR Windows, and because Flex
674     *  has its own concept of popups or pseudo-windows, we
675     *  ignore ACTIVATE and respond to FLEX_WINDOW_ACTIVATE instead
676     */
677    override public function activateHandler(event:Event):void
678    {
679        // block ACTIVATE events
680        if (event.type == Event.ACTIVATE)
681            return;
682
683        super.activateHandler(event);
684
685        // TLF ties activation and focus together.  If a Flex PopUp is created
686        // it is possible to get deactivate/activate events without any
687        // focus events.  If we have focus when we are activated, the selection
688        // state should be SelectionFormatState.FOCUSED not
689        // SelectionFormatState.UNFOCUSED since there might not be a follow on
690        // focusIn event.
691        if (editingMode != EditingMode.READ_ONLY &&
692            textDisplay.getFocus() == textDisplay)
693        {
694            var im:SelectionManager = SelectionManager(beginInteraction());
695            im.setFocus();
696            endInteraction();
697       }
698    }
699
700    /**
701     *  @private
702     *  This handler gets called for DEACTIVATE events from the player
703     *  and FLEX_WINDOW_DEACTIVATE events from Flex.  Because of the
704     *  way AIR handles activation of AIR Windows, and because Flex
705     *  has its own concept of popups or pseudo-windows, we
706     *  ignore DEACTIVATE and respond to FLEX_WINDOW_DEACTIVATE instead
707     */
708    override public function deactivateHandler(event:Event):void
709    {
710        // block DEACTIVATE events
711        if (event.type == Event.DEACTIVATE)
712            return;
713
714        super.deactivateHandler(event);
715    }
716
717    /**
718     *  @private
719     *  sandbox support
720     */
721    override public function beginMouseCapture():void
722    {
723        super.beginMouseCapture();
724        textDisplay.systemManager.getSandboxRoot().addEventListener(SandboxMouseEvent.MOUSE_UP_SOMEWHERE, mouseUpSomewhereHandler);
725        textDisplay.systemManager.getSandboxRoot().addEventListener(SandboxMouseEvent.MOUSE_MOVE_SOMEWHERE, mouseMoveSomewhereHandler);
726    }
727
728    /**
729     *  @private
730     *  sandbox support
731     */
732    override public function endMouseCapture():void
733    {
734        super.endMouseCapture();
735        textDisplay.systemManager.getSandboxRoot().removeEventListener(SandboxMouseEvent.MOUSE_UP_SOMEWHERE, mouseUpSomewhereHandler);
736        textDisplay.systemManager.getSandboxRoot().removeEventListener(SandboxMouseEvent.MOUSE_MOVE_SOMEWHERE, mouseMoveSomewhereHandler);
737    }
738
739    //--------------------------------------------------------------------------
740    //
741    //  Event handlers
742    //
743    //--------------------------------------------------------------------------
744
745    private function mouseUpSomewhereHandler(event:Event):void
746    {
747        mouseUpSomewhere(event);
748    }
749
750    private function mouseMoveSomewhereHandler(event:Event):void
751    {
752        mouseMoveSomewhere(event);
753    }
754}
755
756}
757