blob: 937d3a6c856fbe716b924188901735de56e26b12 [file] [log] [blame]
////////////////////////////////////////////////////////////////////////////////
//
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////
package flashx.textLayout.edit {
import flash.geom.Point;
import flash.geom.Rectangle;
import flash.system.IME;
import flash.text.engine.TextLine;
import flash.text.ime.CompositionAttributeRange;
import flash.text.ime.IIMEClient;
import flashx.textLayout.compose.IFlowComposer;
import flashx.textLayout.compose.TextFlowLine;
import flashx.textLayout.container.ContainerController;
import flashx.textLayout.debug.assert;
import flashx.textLayout.elements.FlowLeafElement;
import flashx.textLayout.elements.TextFlow;
import flashx.textLayout.elements.TextRange;
import flashx.textLayout.formats.BlockProgression;
import flashx.textLayout.formats.IMEStatus;
import flashx.textLayout.formats.ITextLayoutFormat;
import flashx.textLayout.formats.TextLayoutFormat;
import flashx.textLayout.operations.ApplyFormatToElementOperation;
import flashx.textLayout.operations.DeleteTextOperation;
import flashx.textLayout.operations.FlowOperation;
import flashx.textLayout.operations.InsertTextOperation;
import flashx.textLayout.tlf_internal;
import flashx.textLayout.utils.GeometryUtil;
import flashx.undo.IOperation;
import flashx.undo.UndoManager;
use namespace tlf_internal;
internal class IMEClient implements IIMEClient
{
private var _editManager:EditManager;
private var _undoManager:UndoManager;
/** Maintain position of text we've inserted while in the middle of processing IME. */
private var _imeAnchorPosition:int; // start of IME text
private var _imeLength:int; // length of IME text
private var _controller:ContainerController; // controller that had focus at the start of the IME session -- we want this one to keep focus
private var _closing:Boolean;
CONFIG::debug {
private var _imeOperation:IOperation; // IME in-progress edits - used for debugging to confirm that operation we're undoing is the one we did via IME
}
public function IMEClient(editManager:EditManager)
{
_editManager = editManager;
_imeAnchorPosition = _editManager.absoluteStart;
if (_editManager.textFlow)
{
var flowComposer:IFlowComposer = _editManager.textFlow.flowComposer;
if (flowComposer)
{
var controllerIndex:int = flowComposer.findControllerIndexAtPosition(_imeAnchorPosition);
_controller = flowComposer.getControllerAt(controllerIndex);
if (_controller)
_controller.setFocus();
}
}
_closing = false;
if (_editManager.undoManager == null)
{
_undoManager = new UndoManager();
_editManager.setUndoManager(_undoManager);
}
}
/** @private
* Handler function called when the selection has been changed.
* @playerversion Flash 10
* @playerversion AIR 1.5
* @langversion 3.0
*/
public function selectionChanged():void
{
// trace("IMEClient.selectionChanged", _editManager.anchorPosition, _editManager.activePosition);
// If we change the selection to something outside the session, abort the
// session. If we just moved the selection within the session, we tell the IME about the changes.
if (_editManager.absoluteStart > _imeAnchorPosition + _imeLength || _editManager.absoluteEnd < _imeAnchorPosition)
{
//trace("selection changed to out of IME session");
compositionAbandoned();
}
else
{
// This code doesn't with current version of Argo, but should work in future
//trace("selection changed within IME session");
// var imeCompositionSelectionChanged:Function = IME["compositionSelectionChanged"];
// if (IME["compositionSelectionChanged"] !== undefined)
// imeCompositionSelectionChanged(_editManager.absoluteStart - _imeAnchorPosition, _editManager.absoluteEnd - (_imeAnchorPosition + _imeLength));
}
}
private function doIMEClauseOperation(selState:SelectionState, clause:int):void
{
var leaf:FlowLeafElement = _editManager.textFlow.findLeaf(selState.absoluteStart);;
var leafAbsoluteStart:int = leaf.getAbsoluteStart();
var format:TextLayoutFormat = new TextLayoutFormat();
format.setStyle(IMEStatus.IME_CLAUSE, clause.toString());
_editManager.doOperation(new ApplyFormatToElementOperation(selState, leaf, format, selState.absoluteStart - leafAbsoluteStart, selState.absoluteEnd - leafAbsoluteStart));
}
private function doIMEStatusOperation(selState:SelectionState, attrRange:CompositionAttributeRange):void
{
var imeStatus:String;
// Get the IME status from the converted & selected flags
if (attrRange == null)
imeStatus = IMEStatus.DEAD_KEY_INPUT_STATE;
else if (!attrRange.converted)
{
if(!attrRange.selected)
imeStatus = IMEStatus.NOT_SELECTED_RAW;
else
imeStatus = IMEStatus.SELECTED_RAW;
}
else
{
if (!attrRange.selected)
imeStatus = IMEStatus.NOT_SELECTED_CONVERTED;
else
imeStatus = IMEStatus.SELECTED_CONVERTED;
}
// refind since the previous operation changed the spans
var leaf:FlowLeafElement = _editManager.textFlow.findLeaf(selState.absoluteStart);
CONFIG::debug { assert( leaf != null, "found null FlowLeafELement at" + (selState.absoluteStart).toString()); }
var leafAbsoluteStart:int = leaf.getAbsoluteStart();
var format:TextLayoutFormat = new TextLayoutFormat();
format.setStyle(IMEStatus.IME_STATUS, imeStatus);
_editManager.doOperation(new ApplyFormatToElementOperation(selState, leaf, format, selState.absoluteStart - leafAbsoluteStart, selState.absoluteEnd - leafAbsoluteStart));
}
private function deleteIMEText(textFlow:TextFlow):void
{
// Delete any leaves that have IME attributes applied
var leaf:FlowLeafElement = textFlow.getFirstLeaf();
while (leaf)
{
if (leaf.getStyle(IMEStatus.IME_STATUS) !== undefined || leaf.getStyle(IMEStatus.IME_CLAUSE) !== undefined)
{
var leafFormat:TextLayoutFormat = new TextLayoutFormat(leaf.format);
leafFormat.setStyle(IMEStatus.IME_STATUS, undefined);
leafFormat.setStyle(IMEStatus.IME_CLAUSE, undefined);
leaf.format = leafFormat;
var absoluteStart:int = leaf.getAbsoluteStart();
ModelEdit.deleteText(textFlow, absoluteStart, absoluteStart + leaf.textLength, false);
leaf = textFlow.findLeaf(absoluteStart);
}
else
leaf = leaf.getNextLeaf();
}
}
private function rollBackIMEChanges():void
{
// Undo the previous interim ime operation, if there is one. This deletes any text that came in a previous updateComposition call.
// Doing it via undo keeps the undo stack in sync. But if there's been an intervening direct model change, just delete the IME text
// directly. It won't restore what we selected at the beginning of the IME session, but it's the best we can do.
var previousIMEOperation:FlowOperation = _editManager.undoManager.peekUndo() as FlowOperation;
if (_imeLength > 0 && previousIMEOperation && previousIMEOperation.endGeneration == _editManager.textFlow.generation && previousIMEOperation.canUndo())
{
CONFIG::debug { assert(_editManager.undoManager.peekUndo() == _imeOperation, "Unexpected operation in undo stack at end of IME update"); }
if (_editManager.undoManager)
_editManager.undoManager.undo();
CONFIG::debug { assert(_editManager.undoManager.peekRedo() == _imeOperation, "Unexpected operation in redo stack at end of IME session"); }
_editManager.undoManager.popRedo();
}
else // there's been a model change since the last IME change that blocks undo, just find IME text and delete it.
{
_editManager.undoManager.popUndo(); // remove the operation we can't undo
deleteIMEText(_editManager.textFlow);
}
_imeLength = 0; // prevent double deletion
CONFIG::debug { _imeOperation = null; }
}
// IME-related functions
public function updateComposition(text:String, attributes:Vector.<CompositionAttributeRange>, compositionStartIndex:int, compositionEndIndex:int):void
{
// CONFIG::debug { Debugging.traceOut("updateComposition ", compositionStartIndex, compositionEndIndex, text.length); }
// CONFIG::debug { Debugging.traceOut("updateComposition selection ", _editManager.absoluteStart, _editManager.absoluteEnd); }
// Undo the previous interim ime operation, if there is one. This deletes any text that came in a previous updateComposition call.
// Doing it via undo keeps the undo stack in sync.
if (_imeLength > 0)
rollBackIMEChanges();
if (text.length > 0)
{
// Insert the supplied string, using the current editing format.
var pointFormat:ITextLayoutFormat = _editManager.getSelectionState().pointFormat;
var selState:SelectionState = new SelectionState(_editManager.textFlow, _imeAnchorPosition, _imeAnchorPosition + _imeLength, pointFormat);
_editManager.beginIMEOperation();
if (_editManager.absoluteStart != _editManager.absoluteEnd)
_editManager.deleteText(); // delete current selection
var insertOp:InsertTextOperation = new InsertTextOperation(selState, text);
_imeLength = text.length;
_editManager.doOperation(insertOp);
if (attributes && attributes.length > 0)
{
var attrLen:int = attributes.length;
for (var i:int = 0; i < attrLen; i++)
{
var attrRange:CompositionAttributeRange = attributes[i];
var clauseSelState:SelectionState = new SelectionState(_editManager.textFlow, _imeAnchorPosition + attrRange.relativeStart, _imeAnchorPosition + attrRange.relativeEnd);
doIMEClauseOperation(clauseSelState, i);
doIMEStatusOperation(clauseSelState, attrRange);
}
}
else // composing accented characters
{
clauseSelState = new SelectionState(_editManager.textFlow, _imeAnchorPosition, _imeAnchorPosition + _imeLength, pointFormat);
doIMEClauseOperation(clauseSelState, 0);
doIMEStatusOperation(clauseSelState, null);
}
var newSelectionStart:int = _imeAnchorPosition + compositionStartIndex;
var newSelectionEnd:int = _imeAnchorPosition + compositionEndIndex;
if (_editManager.absoluteStart != newSelectionStart || _editManager.absoluteEnd != newSelectionEnd)
{
_editManager.selectRange(_imeAnchorPosition + compositionStartIndex, _imeAnchorPosition + compositionEndIndex);
}
CONFIG::debug { _imeOperation = null; }
_editManager.endIMEOperation();
CONFIG::debug { _imeOperation = _editManager.undoManager.peekUndo(); }
}
}
public function confirmComposition(text:String = null, preserveSelection:Boolean = false):void
{
// trace("confirmComposition", text, preserveSelection);
endIMESession();
}
public function compositionAbandoned():void
{
// trace("compositionAbandoned");
// In Argo we could just do this:
// IME.compositionAbandoned();
// but for support in Astro/Squirt where this API is undefined we do this:
var imeCompositionAbandoned:Function = IME["compositionAbandoned"];
if (IME["compositionAbandoned"] !== undefined)
imeCompositionAbandoned();
}
private function endIMESession():void
{
if (!_editManager || _closing)
return;
// trace("end IME session");
_closing = true;
// Undo the IME operation. We're going to re-add the text, without all the special attributes, as part of handling
// the textInput event that comes next.
if (_imeLength > 0)
rollBackIMEChanges();
if (_undoManager)
_editManager.setUndoManager(null);
// Clear IME state - tell EditManager to release IMEClient to finally close session
_editManager.endIMESession();
_editManager = null;
}
CONFIG::debug
{ // debugging code for displaying IME bounds rectangle
import flash.display.Sprite;
import flash.display.Graphics;
private function displayRectInContainer(container:Sprite, r:Rectangle):void
{
var g:Graphics = container.graphics;
g.beginFill(0xff0000);
g.moveTo(r.x, r.y);
g.lineTo(r.right, r.y);
g.lineTo(r.right, r.bottom);
g.lineTo(r.x, r.bottom);
g.lineTo(r.x, r.y);
g.endFill();
}
}
public function getTextBounds(startIndex:int, endIndex:int):Rectangle
{
if(startIndex >= 0 && startIndex < _editManager.textFlow.textLength && endIndex >= 0 && endIndex < _editManager.textFlow.textLength)
{
if (startIndex != endIndex)
{
var boundsResult:Array = GeometryUtil.getHighlightBounds(new TextRange(_editManager.textFlow, startIndex, endIndex));
//bail out if we don't have any results to show
if (boundsResult.length > 0)
{
var bounds:Rectangle = boundsResult[0].rect;
var textLine:TextLine = boundsResult[0].textLine;
var resultTopLeft:Point = textLine.localToGlobal(bounds.topLeft);
var resultBottomRight:Point = textLine.localToGlobal(bounds.bottomRight);
if (textLine.parent)
{
var containerTopLeft:Point = textLine.parent.globalToLocal(resultTopLeft);
var containerBottomLeft:Point = textLine.parent.globalToLocal(resultBottomRight);
// CONFIG::debug { displayRectInContainer(Sprite(textLine.parent), new Rectangle(containerTopLeft.x, containerTopLeft.y, containerBottomLeft.x - containerTopLeft.x, containerBottomLeft.y - containerTopLeft.y));}
return new Rectangle(containerTopLeft.x, containerTopLeft.y, containerBottomLeft.x - containerTopLeft.x, containerBottomLeft.y - containerTopLeft.y);
}
}
}
else
{
var flowComposer:IFlowComposer = _editManager.textFlow.flowComposer;
var lineIndex:int = flowComposer.findLineIndexAtPosition(startIndex);
// Stick to the end of the last line
if (lineIndex == flowComposer.numLines)
lineIndex--;
if (flowComposer.getLineAt(lineIndex).controller == _controller)
{
var line:TextFlowLine = flowComposer.getLineAt(lineIndex);
var previousLine:TextFlowLine = lineIndex != 0 ? flowComposer.getLineAt(lineIndex-1) : null;
var nextLine:TextFlowLine = lineIndex != flowComposer.numLines-1 ? flowComposer.getLineAt(lineIndex+1) : null;
// CONFIG::debug { displayRectInContainer(_controller.container, line.computePointSelectionRectangle(startIndex, _controller.container, previousLine, nextLine, true));}
return line.computePointSelectionRectangle(startIndex, _controller.container, previousLine, nextLine, true);
}
}
}
return new Rectangle(0,0,0,0);
}
public function get compositionStartIndex():int
{
// trace("compositionStartIndex");
return _imeAnchorPosition;
}
public function get compositionEndIndex():int
{
// trace("compositionEndIndex");
return _imeAnchorPosition + _imeLength;
}
public function get verticalTextLayout():Boolean
{
// trace("verticalTextLayout ", _editManager.textFlow.computedFormat.blockProgression == BlockProgression.RL ? "true" : "false");
return _editManager.textFlow.computedFormat.blockProgression == BlockProgression.RL;
}
public function get selectionActiveIndex():int
{
//trace("selectionActiveIndex");
return _editManager.activePosition;
}
public function get selectionAnchorIndex():int
{
//trace("selectionAnchorIndex");
return _editManager.anchorPosition;
}
public function selectRange(anchorIndex:int, activeIndex:int):void
{
_editManager.selectRange(anchorIndex, activeIndex);
}
public function setFocus():void
{
if (_controller && _controller.container && _controller.container.stage && _controller.container.stage.focus != _controller.container)
_controller.setFocus();
}
/**
* Gets the specified range of text from a component implementing ITextSupport.
* To retrieve all text in the component, do not specify values for <code>startIndex</code> and <code>endIndex</code>.
* Components which wish to support inline IME or web searchability should call into this method.
* Components overriding this method should ensure that the default values of <code>-1</code>
* for <code>startIndex</code> and <code>endIndex</code> are supported.
*
* @playerversion Flash 10.0
* @langversion 3.0
*/
public function getTextInRange(startIndex:int, endIndex:int):String
{
//trace("getTextInRange");
// Check for valid indices
var textFlow:TextFlow = _editManager.textFlow;
if (startIndex < -1 || endIndex < -1 || startIndex > (textFlow.textLength - 1) || endIndex > (textFlow.textLength - 1))
return null;
// Make sure they're in the right order
if (endIndex < startIndex)
{
var tempIndex:int = endIndex;
endIndex = startIndex;
startIndex = tempIndex;
}
if (startIndex == -1)
startIndex = 0;
return textFlow.getText(startIndex, endIndex);
}
}
}