blob: 8d96340781f6761ec401e3b561be9bcb7f9edb10 [file] [log] [blame]
////////////////////////////////////////////////////////////////////////////////
//
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////
package spark.components.supportClasses
{
import flash.display.BlendMode;
import flash.display.Graphics;
import flash.events.Event;
import flash.events.FocusEvent;
import flash.events.KeyboardEvent;
import flash.events.MouseEvent;
import flash.geom.Rectangle;
import flash.ui.ContextMenu;
import flash.ui.Keyboard;
import mx.core.mx_internal;
import mx.events.SandboxMouseEvent;
import mx.styles.IStyleClient;
import spark.components.RichEditableText;
import spark.components.TextSelectionHighlighting;
import flashx.textLayout.tlf_internal;
import flashx.textLayout.container.ContainerController;
import flashx.textLayout.container.TextContainerManager;
import flashx.textLayout.edit.EditManager;
import flashx.textLayout.edit.EditingMode;
import flashx.textLayout.edit.ElementRange;
import flashx.textLayout.edit.IEditManager;
import flashx.textLayout.edit.ISelectionManager;
import flashx.textLayout.edit.SelectionFormat;
import flashx.textLayout.edit.SelectionManager;
import flashx.textLayout.edit.SelectionState;
import flashx.textLayout.elements.FlowLeafElement;
import flashx.textLayout.elements.IConfiguration;
import flashx.textLayout.elements.ParagraphElement;
import flashx.textLayout.elements.TextFlow;
import flashx.textLayout.elements.TextRange;
import flashx.textLayout.events.SelectionEvent;
import flashx.textLayout.formats.Category;
import flashx.textLayout.formats.ITextLayoutFormat;
import flashx.textLayout.formats.TextLayoutFormat;
import flashx.textLayout.operations.ApplyFormatOperation;
import flashx.textLayout.operations.InsertTextOperation;
import flashx.textLayout.property.Property;
import flashx.undo.IUndoManager;
import flashx.undo.UndoManager;
use namespace mx_internal;
use namespace tlf_internal;
[ExcludeClass]
/**
* @private
* A subclass of TextContainerManager that manages the text in
* a RichEditableText component.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public class RichEditableTextContainerManager extends TextContainerManager
{
//--------------------------------------------------------------------------
//
// Class Variables
//
//--------------------------------------------------------------------------
/**
* @private
* Disables blinking cursor so mustella test snapshots don't get intermittent
* cursors.
*/
mx_internal static var hideCursor:Boolean = false;
/**
* Constructor.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function RichEditableTextContainerManager(
container:RichEditableText,
configuration:IConfiguration=null)
{
super(container, configuration);
textDisplay = container;
}
//--------------------------------------------------------------------------
//
// Variables
//
//--------------------------------------------------------------------------
/**
* @private
*/
private var hasScrollRect:Boolean = false;
/**
* @private
*/
private var textDisplay:RichEditableText;
//--------------------------------------------------------------------------
//
// Overridden methods
//
//--------------------------------------------------------------------------
/**
* @private
*/
override public function drawBackgroundAndSetScrollRect(
scrollX:Number, scrollY:Number):Boolean
{
// If not auto-sizing these are the same as the compositionWidth/Height.
// If auto-sizing, the compositionWidth/Height may be NaN. If no
// constraints this will reflect the actual size of the text.
var width:Number = textDisplay.width;
var height:Number = textDisplay.height;
var contentBounds:Rectangle = getContentBounds();
// If measuring width, use the content width.
if (isNaN(width))
width = contentBounds.right;
// If measuring height, use the content height.
if (isNaN(height))
height = contentBounds.bottom;
// TODO:(cframpto) Adjust for RL text.
// See ContainerController.updateVisibleRectangle().
// (effectiveBlockProgression == BlockProgression.RL) ? -width : 0;
var xOrigin:Number = 0;
// If autoSize, and lineBreak="toFit" there should never be
// a scroll rect but if lineBreak="explicit" the text may need
// to be clipped.
if (scrollX == 0 && scrollY == 0 &&
contentBounds.left >= xOrigin &&
contentBounds.right <= width &&
contentBounds.top >= 0 &&
contentBounds.bottom <= height)
{
// skip the scrollRect
if (hasScrollRect)
{
container.scrollRect = null;
hasScrollRect = false;
}
}
else
{
container.scrollRect = new Rectangle(scrollX, scrollY, width, height);
hasScrollRect = true;
}
// Client must draw a background to get mouse events,
// even it if is 100% transparent.
// If backgroundColor is defined, fill the bounds of the component
// with backgroundColor drawn with alpha level backgroundAlpha.
// Otherwise, fill with transparent black.
// (The color in this case is irrelevant.)
var color:uint = 0x000000;
var alpha:Number = 0.0;
var styleableContainer:IStyleClient = container as IStyleClient;
if (styleableContainer)
{
var backgroundColor:* =
styleableContainer.getStyle("backgroundColor");
if (backgroundColor !== undefined)
{
color = uint(backgroundColor);
alpha = styleableContainer.getStyle("backgroundAlpha");
}
}
// TODO (cframpto): Adjust for RL text. See
// ContainerController.attachTransparentBackgroundForHit().
var g:Graphics = container.graphics;
g.clear();
g.lineStyle();
g.beginFill(color, alpha);
g.drawRect(scrollX, scrollY, width, height);
g.endFill();
return hasScrollRect;
}
/**
* @private
*
* If the user specified a custom context menu then use
* it rather than the default context menu. It must be set before the
* first mouse over/mouse hover or foucsIn event to be used.
*
* TLF will remove the context menu when it switches from the factory
* to the composer and the controller will then request it again.
*/
override tlf_internal function getContextMenu():ContextMenu
{
// ToDo(cframpto): Ideally could specify the context
// menu on the TextArea or the TextInput and it wouldn't be obscured
// by TLF's context menu.
// Return null to use the existing contextMenu on the container.
// Otherwise the TCM will overwrite this contextMenu.
return textDisplay.contextMenu != null ? null : super.getContextMenu();
}
/**
* @private
*/
override protected function getUndoManager():IUndoManager
{
if (!textDisplay.undoManager)
{
textDisplay.undoManager = new UndoManager();
textDisplay.undoManager.undoAndRedoItemLimit = int.MAX_VALUE;
}
return textDisplay.undoManager;
}
/**
* @private
*/
override protected function getFocusedSelectionFormat():SelectionFormat
{
var selectionColor:* = textDisplay.getStyle("focusedTextSelectionColor");
var focusedPointAlpha:Number =
editingMode == EditingMode.READ_WRITE ?
1.0 :
0.0;
// If editable, the insertion point is black, inverted, which makes it
// the inverse color of the background, for maximum readability.
// If not editable, then no insertion point.
return new SelectionFormat(
selectionColor, 1.0, BlendMode.NORMAL,
0x000000, hideCursor ? 0 : focusedPointAlpha, BlendMode.INVERT);
}
/**
* @private
*/
override protected function getUnfocusedSelectionFormat():SelectionFormat
{
var unfocusedSelectionColor:* = textDisplay.getStyle(
"unfocusedTextSelectionColor");
var unfocusedAlpha:Number =
textDisplay.selectionHighlighting !=
TextSelectionHighlighting.WHEN_FOCUSED ?
1.0 :
0.0;
// No insertion point when no focus.
return new SelectionFormat(
unfocusedSelectionColor, unfocusedAlpha, BlendMode.NORMAL,
unfocusedSelectionColor, 0.0);
}
/**
* @private
*/
override protected function getInactiveSelectionFormat():SelectionFormat
{
var inactiveSelectionColor:* = textDisplay.getStyle(
"inactiveTextSelectionColor");
var inactivePointAlpha:Number = 0.0;
var inactiveRangeAlpha:Number =
textDisplay.selectionHighlighting ==
TextSelectionHighlighting.ALWAYS ?
1.0 :
0.0;
// This doesn't really matter since inactivePointAlpha is 0.
var pointBlinkRate:Number = 0.0;
return new SelectionFormat(
inactiveSelectionColor, inactiveRangeAlpha, BlendMode.NORMAL,
inactiveSelectionColor, inactivePointAlpha, BlendMode.INVERT,
pointBlinkRate);
}
/**
* @private
*/
override protected function createEditManager(
undoManager:flashx.undo.IUndoManager):IEditManager
{
var editManager:IEditManager = super.createEditManager(undoManager);
// Default is to batch text input. If the component, like ComboBox
// wants to act on each keystroke then set this to false.
editManager.allowDelayedOperations = textDisplay.batchTextInput;
// Do not delayUpdates until further work is done to ensure our public API methods to
// format and insert text work correctly. TLF does not dispatch the selectionChange
// event until the display is updated which means our selection properties may not
// be in sync with the TLF values. This could matter for our API methods that
// take the selection as parameters or default to the current selection. In the
// former case, the user could query for the selection or rely on the selectionChange
// event and get incorrect values if there is a pending update and in the later case
// we fill in the default selection which might not be current if there is a pending
// update.
editManager.delayUpdates = false;
return editManager;
}
/**
* @private
*/
override public function setText(text:String):void
{
super.setText(text);
// If we have focus, need to make sure we can still input text.
initForInputIfHaveFocus();
}
/**
* @private
*/
override public function setTextFlow(textFlow:TextFlow):void
{
super.setTextFlow(textFlow);
// If we have focus, need to make sure we can still input text.
initForInputIfHaveFocus();
}
/**
* @private
*/
private function initForInputIfHaveFocus():void
{
// If we have focus, need to make sure there is a composer in place,
// the new controller knows it has focus, and there is an insertion
// point so input works without a mouse over or mouse click. Normally
// this is done in our focusIn handler by making sure there is a
// selection. Test this by clicking an arrow in the NumericStepper
// and then entering a number without clicking on the input field first.
if (editingMode != EditingMode.READ_ONLY &&
textDisplay.getFocus() == textDisplay)
{
// this will ensure a text flow with a comopser
var im:ISelectionManager = beginInteraction();
var controller:ContainerController =
getTextFlow().flowComposer.getControllerAt(0);
controller.requiredFocusInHandler(null);
if (!preserveSelectionOnSetText)
im.selectRange(0, 0);
endInteraction();
}
}
/**
* @private
* To apply a format to a selection in a textFlow without using the
* selection manager.
*/
mx_internal function applyFormatOperation(
leafFormat:ITextLayoutFormat,
paragraphFormat:ITextLayoutFormat,
containerFormat:ITextLayoutFormat,
anchorPosition:int,
activePosition:int):Boolean
{
// Nothing to do.
if (anchorPosition == -1 || activePosition == -1)
return true;
var textFlow:TextFlow = getTextFlowWithComposer();
var operationState:SelectionState =
new SelectionState(textFlow, anchorPosition, activePosition);
// If using the edit manager and the selection is the current selection,
// need to set the flag so point selection is set with pending formats for next
// char typed.
const editManager:IEditManager = textFlow.interactionManager as IEditManager;
if (editManager)
{
const absoluteStart:int = getAbsoluteStart(anchorPosition, activePosition);
const absoluteEnd:int = getAbsoluteEnd(anchorPosition, activePosition);
if (editManager.absoluteStart == absoluteStart && editManager.absoluteEnd == absoluteEnd)
operationState.selectionManagerOperationState = true;
}
// For the case when interactive editing is not allowed.
var op:ApplyFormatOperation =
new ApplyFormatOperation(
operationState, leafFormat, paragraphFormat, containerFormat);
var success:Boolean = op.doOperation();
if (success)
{
textFlow.normalize();
textFlow.flowComposer.updateAllControllers();
}
return success;
}
/**
* @private
* To get the format of a character. Our API allows this operation even
* when the editingMode does not permit either interactive selection or
* editing.
*/
mx_internal function getCommonCharacterFormat(
anchorPosition:int,
activePosition:int):ITextLayoutFormat
{
if (anchorPosition == -1 || activePosition == -1)
return null;
var textFlow:TextFlow = getTextFlowWithComposer();
if (textFlow.interactionManager)
{
// If there is a selection manager use it so that the format,
// depending on the range, may include any attributes set on a point
// selection but not yet applied.
const range:TextRange =
new TextRange(textFlow, anchorPosition, activePosition);
return textFlow.interactionManager.getCommonCharacterFormat(range);
}
else
{
// ElementRange will order the selection points. Since there isn't
// an interactionManager there is not a point selection to worry
// about.
var selRange:ElementRange =
ElementRange.createElementRange(textFlow, anchorPosition, activePosition);
return selRange.getCommonCharacterFormat();
}
}
/**
* @private
* To get the format of the container without using a SelectionManager.
* The method should be kept in sync with the version in the
* SelectionManager.
*/
mx_internal function getCommonContainerFormat():ITextLayoutFormat
{
var textFlow:TextFlow = getTextFlowWithComposer();
// absoluteStart and absoluteEnd values not used.
var selRange:ElementRange =
ElementRange.createElementRange(textFlow, 0, textFlow.textLength - 1);
return selRange.getCommonContainerFormat();
}
/**
* @private
* To get the format of a paragraph without using a SelectionManager.
* The method should be kept in sync with the version in the
* SelectionManager.
*/
mx_internal function getCommonParagraphFormat(
anchorPosition:int,
activePosition:int):ITextLayoutFormat
{
if (anchorPosition == -1 || activePosition == -1)
return null;
var textFlow:TextFlow = getTextFlowWithComposer();
// ElementRange will order the selection points.
var selRange:ElementRange =
ElementRange.createElementRange(textFlow, anchorPosition, activePosition);
return selRange.getCommonParagraphFormat();
}
/**
* @private
* Insert or append text to the textFlow without using an EditManager.
* If there is a SelectionManager or EditManager its selection will be
* updated at the end of the operation to keep it in sync.
*/
mx_internal function insertTextOperation(insertText:String,
anchorPosition:int,
activePosition:int):Boolean
{
// No insertion point.
if (anchorPosition == -1 || activePosition == -1)
return false;
var textFlow:TextFlow = getTextFlowWithComposer();
var absoluteStart:int = getAbsoluteStart(anchorPosition, activePosition);
var absoluteEnd:int = getAbsoluteEnd(anchorPosition, activePosition);
// Need to get the format of the insertion point so that the inserted
// text will have this format.
var pointFormat:ITextLayoutFormat =
getCommonCharacterFormat(absoluteStart, absoluteStart);
var operationState:SelectionState =
new SelectionState(textFlow, absoluteStart, absoluteEnd, pointFormat);
// If there is an interaction manager, this keeps it in sync with
// the results of this operation.
operationState.selectionManagerOperationState = true;
var op:InsertTextOperation =
new InsertTextOperation(operationState, insertText);
// Generations don't seem to be used in this code path since we
// aren't doing composite, merge or undo operations so they were
// optimized out.
var success:Boolean = op.doOperation();
if (success)
{
textFlow.normalize();
textFlow.flowComposer.updateAllControllers();
var insertPt:int = absoluteEnd - (absoluteEnd - absoluteStart) +
+ insertText.length;
// No point format.
var selectionState:SelectionState =
new SelectionState(textFlow, insertPt, insertPt);
var selectionEvent:SelectionEvent =
new SelectionEvent(SelectionEvent.SELECTION_CHANGE,
false, false, selectionState);
textFlow.dispatchEvent(selectionEvent);
scrollToRange(insertPt, insertPt);
}
return success;
}
/**
* Note: It is probably a TLF bug that, if delayedUpdates is true, we have to call
* updateAllControllers before doing a format operation to guarantee the correct
* results.
*/
mx_internal function getTextFlowWithComposer():TextFlow
{
var textFlow:TextFlow = getTextFlow();
// Make sure there is a text flow with a flow composer. There will
// not be an interaction manager if editingMode is read-only. If
// there is an interaction manager flush any pending inserts into the
// text flow unless we are delaying updates in which case we may have to finish
// composition.
if (composeState != TextContainerManager.COMPOSE_COMPOSER)
{
convertToTextFlowWithComposer();
}
else if (textFlow.interactionManager)
{
const editManager:IEditManager = textFlow.interactionManager as IEditManager;
if (editManager && editManager.delayUpdates)
editManager.updateAllControllers();
else
textFlow.interactionManager.flushPendingOperations();
}
return textFlow;
}
/**
* @private
*/
private function getAbsoluteStart(anchorPosition:int, activePosition:int):int
{
return (anchorPosition < activePosition) ?
anchorPosition : activePosition;
}
/**
* @private
*/
private function getAbsoluteEnd(anchorPosition:int, activePosition:int):int
{
return (anchorPosition > activePosition) ?
anchorPosition : activePosition;
}
//--------------------------------------------------------------------------
//
// Overridden event handlers
//
//--------------------------------------------------------------------------
/**
* @private
*/
override public function focusInHandler(event:FocusEvent):void
{
textDisplay.focusInHandler(event);
super.focusInHandler(event);
}
/**
* @private
*/
override public function focusOutHandler(event:FocusEvent):void
{
textDisplay.focusOutHandler(event);
super.focusOutHandler(event);
}
/**
* @private
*/
override public function keyDownHandler(event:KeyboardEvent):void
{
textDisplay.keyDownHandler(event);
if (!event.isDefaultPrevented())
{
var clone:KeyboardEvent = KeyboardEvent(event.clone());
super.keyDownHandler(clone);
if (clone.isDefaultPrevented())
event.preventDefault();
}
}
/**
* @private
*/
override public function keyUpHandler(event:KeyboardEvent):void
{
if (!event.isDefaultPrevented())
{
var clone:KeyboardEvent = KeyboardEvent(event.clone());
super.keyUpHandler(clone);
if (clone.isDefaultPrevented())
event.preventDefault();
}
}
/**
* @private
*/
override public function mouseDownHandler(event:MouseEvent):void
{
textDisplay.mouseDownHandler(event);
super.mouseDownHandler(event);
}
/**
* @private
* This handler gets called for ACTIVATE events from the player
* and FLEX_WINDOW_ACTIVATE events from Flex. Because of the
* way AIR handles activation of AIR Windows, and because Flex
* has its own concept of popups or pseudo-windows, we
* ignore ACTIVATE and respond to FLEX_WINDOW_ACTIVATE instead
*/
override public function activateHandler(event:Event):void
{
// block ACTIVATE events
if (event.type == Event.ACTIVATE)
return;
super.activateHandler(event);
// TLF ties activation and focus together. If a Flex PopUp is created
// it is possible to get deactivate/activate events without any
// focus events. If we have focus when we are activated, the selection
// state should be SelectionFormatState.FOCUSED not
// SelectionFormatState.UNFOCUSED since there might not be a follow on
// focusIn event.
if (editingMode != EditingMode.READ_ONLY &&
textDisplay.getFocus() == textDisplay)
{
var im:SelectionManager = SelectionManager(beginInteraction());
im.setFocus();
endInteraction();
}
}
/**
* @private
* This handler gets called for DEACTIVATE events from the player
* and FLEX_WINDOW_DEACTIVATE events from Flex. Because of the
* way AIR handles activation of AIR Windows, and because Flex
* has its own concept of popups or pseudo-windows, we
* ignore DEACTIVATE and respond to FLEX_WINDOW_DEACTIVATE instead
*/
override public function deactivateHandler(event:Event):void
{
// block DEACTIVATE events
if (event.type == Event.DEACTIVATE)
return;
super.deactivateHandler(event);
}
/**
* @private
* sandbox support
*/
override public function beginMouseCapture():void
{
super.beginMouseCapture();
textDisplay.systemManager.getSandboxRoot().addEventListener(SandboxMouseEvent.MOUSE_UP_SOMEWHERE, mouseUpSomewhereHandler);
textDisplay.systemManager.getSandboxRoot().addEventListener(SandboxMouseEvent.MOUSE_MOVE_SOMEWHERE, mouseMoveSomewhereHandler);
}
/**
* @private
* sandbox support
*/
override public function endMouseCapture():void
{
super.endMouseCapture();
textDisplay.systemManager.getSandboxRoot().removeEventListener(SandboxMouseEvent.MOUSE_UP_SOMEWHERE, mouseUpSomewhereHandler);
textDisplay.systemManager.getSandboxRoot().removeEventListener(SandboxMouseEvent.MOUSE_MOVE_SOMEWHERE, mouseMoveSomewhereHandler);
}
//--------------------------------------------------------------------------
//
// Event handlers
//
//--------------------------------------------------------------------------
private function mouseUpSomewhereHandler(event:Event):void
{
mouseUpSomewhere(event);
}
private function mouseMoveSomewhereHandler(event:Event):void
{
mouseMoveSomewhere(event);
}
}
}