blob: 00333203ddd5137e79fe23950ba4f8c49dc1b6c2 [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 com.adobe.linguistics.spelling
{
import com.adobe.linguistics.spelling.framework.SpellingConfiguration;
import com.adobe.linguistics.spelling.framework.SpellingService;
import com.adobe.linguistics.spelling.framework.ui.HaloHighlighter;
import com.adobe.linguistics.spelling.framework.ui.HaloWordProcessor;
import com.adobe.linguistics.spelling.framework.ui.IHighlighter;
import com.adobe.linguistics.spelling.framework.ui.IWordProcessor;
import com.adobe.linguistics.spelling.framework.ui.SparkHighlighter;
import com.adobe.linguistics.spelling.framework.ui.SparkWordProcessor;
import com.adobe.linguistics.utils.TextTokenizer;
import com.adobe.linguistics.utils.Token;
import flash.events.Event;
import flash.events.FocusEvent;
import flash.geom.Point;
import flash.geom.Rectangle;
import flash.net.SharedObject;
import flash.net.URLLoader;
import flash.net.URLRequest;
import flash.text.TextField;
import flash.utils.Dictionary;
import flashx.textLayout.edit.SelectionManager;
import flashx.textLayout.elements.TextFlow;
import flashx.textLayout.tlf_internal;
import mx.controls.RichTextEditor;
import mx.controls.TextArea;
import mx.controls.TextInput;
import mx.core.UIComponent;
import mx.core.mx_internal;
import mx.events.ScrollEvent;
import spark.components.RichEditableText;
import spark.components.TextArea;
import spark.components.TextInput;
use namespace mx_internal;
use namespace tlf_internal;
/**
* UI implementation for spelling.
*
* <p>This class is a simple UI for some standard Flex UI components.
* It is not intended to address a complete user interface.
* Instead, it presents a basic user interface for some commonly used Flex UI components.</p>
*
* <p>For advanced text editing applications, more complex features are likely required.
* For those applications, we recommend bypassing this class and utilizing the <code>SpellChecker</code> class directly.</p>
*
* @playerversion Flash 10
* @langversion 3.0
*/
public class SpellUI
{
private var hh:IHighlighter;
private var hw:IWordProcessor;
private var _checkLastWord:Boolean = true;
private var _spellingEnabled:Boolean;
private var _actualParent:*;
private var isHaloComponent:Boolean;
private var isSparkComponent:Boolean;
//New Added below
private var mTextField:*;
//private var mTextField:RichEditableText;
private var _dictionaryName:String = "";
private var _userDictionary:UserDictionary = null;
private var _sharedObject:SharedObject = null;
private var _scm:SpellingContextMenu;
private var _resource_locale:Object = null;
private var _spellingService:SpellingService = null;
private static var _contextMenuEntries:Object = {enable:"Enable Spelling", disable:"Disable Spelling", add:"Add to dictionary"};
private static var _spellingConfigUrl:String = "SpellingConfig.xml";
private static var _UITable:Dictionary= new Dictionary();
private static var _parentTable:Dictionary= new Dictionary();
private static var _cacheDictTable:Dictionary= new Dictionary();
private static var _configXML:XML = null;
private static var _configXMLLoading:Boolean = false;
private static var _configXMLLoader:URLLoader = new URLLoader();
// Work around for the memory usage problem, ideally a better fix is to provide a dictionary unload function
private static var _cache:Object = {};
/**
* Enables the spell checking feature for a UI component.
*
* <p>Note: This version provides only enabling function but no disabling function.</p>
*
* @param comp A text editing Flex UI component.
It can be a <code>TextArea</code>, <code>TextInput</code>, <code>RichTextEditor</code>
(both spark and mx), or a <code>RichTextEditor</code>.
* @param lang The language of the dictionary to use when checking the text spelling in <code>comp</code>.
*
* @includeExample Examples/Flex/SquigglyUIExample/src/SquigglyUIExample.mxml
* @playerversion Flash 10
* @langversion 3.0
*/
public static function enableSpelling(comp:UIComponent, lang:String):void
{
if ( lang == null ) return;
// TODO: Change dict parameter type to a SpellCheck class or a URL string.
var txt:* = getComponentTextModel(comp);
if ( txt==null || _UITable[comp]!=undefined )
return;
// TODO: dangerous, is garbage collection going to clear this?
_UITable[comp] = new SpellUI(txt, lang);
_parentTable[txt] = comp;
_cacheDictTable[comp]=lang;
}
// Customize the spelling related entry in spelling contextMenu
public static function setSpellingMenuEntries(entries:Object):Boolean
{
if (entries.enable && entries.disable && entries.add && (entries.enable != "") && (entries.disable != "") && (entries.add != ""))
{
_contextMenuEntries = entries;
return true;
}
else
return false;
}
/**
* Get the spelling context menu entries.
*
* @return A flex <code>Object</code> containing the spelling context menu entries. If you haven't customized the entries, you get the default associative array <code>{enable:"Enable Spelling", disable:"Disable Spelling", add:"Add to dictionary"}</code>
* @playerversion Flash 10
* @langversion 3.0
*/
public static function getSpellingMenuEntries():Object
{
return _contextMenuEntries;
}
/**
* The URL for the spelling config xml file. If you haven't specify it, the default URL is [applicationDirectory]/SpellingConfig.xml. Note that we don't validate the URL, if the file doesn't exist, you will get an error when calling enableSpelling() function.
*
* @example The following code customize the spellingConfigUrl before enabling spell checking.
* <listing version="3.0">
* SpellUI.spellingConfigUrl = "./config/MySpellingConfig.xml";
* SpellUI.enableSpelling(textArea, "en_US");
* </listing>
*/
public static function get spellingConfigUrl():String
{
return _spellingConfigUrl;
}
public static function set spellingConfigUrl(url:String):void
{
if (url == null) throw new Error("URL can't be null");
_spellingConfigUrl = url;
}
/**
* Disable the spell checking feature for a UI component.
*
* @param comp A text editing Flex UI component.
* @playerversion Flash 10
* @langversion 3.0
*/
public static function disableSpelling(comp:UIComponent):void{
if ( _UITable[comp] == undefined )
return;
var _ui:SpellUI = _UITable[comp];
if ( _ui != null) _ui.cleanUp();
var dictName:String = _cacheDictTable[comp];
var cleanUPDictionaryCount:int = 0;
for each ( var _dictName:String in _cacheDictTable ) {
if ( _dictName == dictName )
cleanUPDictionaryCount++;
}
if ( cleanUPDictionaryCount == 1 ) {
_cache[dictName] = undefined;
}
delete _UITable[comp];
delete _cacheDictTable[comp];
}
/**
@private
(This property is for Squiggly Developer use only.)
*/
public static function get UITable():Dictionary {
return _UITable;
}
/**
@private
(This property is for Squiggly Developer use only.)
*/
public function set spellingEnabled(value:Boolean):void {
_spellingEnabled = value;
}
/**
@private
(This property is for Squiggly Developer use only.)
*/
public static function get parentComp():Dictionary {
return _parentTable;
}
private static function getComponentTextModel(comp:UIComponent):* {
var txt:TextField = null;
var txt2:RichEditableText = null;
if ( (comp == null) || !( (comp is mx.controls.TextArea) || (comp is mx.controls.TextInput) || (comp is RichTextEditor)
|| (comp is spark.components.TextArea) || (comp is spark.components.TextInput) || (comp is RichEditableText)) )
return null;
if ((comp as RichTextEditor) != null) {
txt = (comp as RichTextEditor).textArea.getTextField() as TextField;
}
else if ((comp as mx.controls.TextArea) != null){
txt = (comp as mx.controls.TextArea).getTextField() as TextField;
}
else if ((comp as mx.controls.TextInput) != null) {
txt = (comp as mx.controls.TextInput).getTextField() as TextField;
}
else if ((comp as spark.components.TextArea) != null) {
if ((comp as spark.components.TextArea).textDisplay is TextField)
txt = (comp as spark.components.TextArea).textDisplay as TextField;
else
txt2 = (comp as spark.components.TextArea).textDisplay as RichEditableText;
}
else if ((comp as spark.components.TextInput) != null) {
if ((comp as spark.components.TextInput).textDisplay is TextField)
txt = (comp as spark.components.TextInput).textDisplay as TextField;
else
txt2 = (comp as spark.components.TextInput).textDisplay as RichEditableText;
}
else if ((comp as RichEditableText) !=null) {
txt2 = comp as RichEditableText;
}
else {
// do nothing if it's not a valid text component
return null;
}
if (txt != null)
return txt;
else
return txt2;
}
/**
* Constructs a SpellUI object.
*
* @param textModel A Flex UI component to include spell-check capability.
* @param lang The language code to use for spell checking.
*
* @playerversion Flash 10
* @langversion 3.0
*/
public function SpellUI(textModel:*, lang:String)
{
// TODO: Consider making this method invisible to user, only expose the static function.
if ( textModel is TextField ) {
isHaloComponent=true;
//New Added below -- check if text field needs to be extracted from textModel
//mTextField = textModel;
}else if (textModel is RichEditableText ) {
isSparkComponent=true;
textModel.setFocus();
textModel.selectRange(textModel.text.length, textModel.text.length);
//New Added below -- check if text field needs to be extracted from textModel
//mTextField = textModel ;
}else {
// do nothing, we only accept textField and TextFlow here....
return;
}
_actualParent = textModel;
mTextField = textModel;
mTextField.addEventListener(FocusEvent.FOCUS_OUT, handleFocusOut);
mTextField.addEventListener(FocusEvent.FOCUS_IN, handleFocusIn);
mTextField.addEventListener(ScrollEvent.SCROLL, spellCheckScreen);
mTextField.parent.addEventListener(Event.RENDER, spellCheckScreen);
mTextField.parent.addEventListener(Event.CHANGE, handleChangeEvent);
_dictionaryName = lang;
loadConfig();
}
private function spellCheckScreen(event:Event):void
{
doSpellingJob();
}
private function handleFocusOut(event:FocusEvent):void
{
_checkLastWord = true;
doSpellingJob();
}
private function handleFocusIn(event:FocusEvent):void
{
_checkLastWord = false;
doSpellingJob();
}
private function handleChangeEvent( event:Event ) :void {
_checkLastWord = false;
spellCheckScreen(event);
}
public function doSpellingJob():void
{
if (_spellingEnabled == false)
return;
if(!isTextFieldReadyForSpellingJob())
return;
spellCheckRange(getValidFirstWordIndex(), getValidLastWordIndex());
}
private function isTextFieldReadyForSpellingJob():Boolean
{
if(!mTextField)
return false;
var textFlow:TextFlow = mTextField is RichEditableText ? RichEditableText(mTextField).textFlow : null;
if(!textFlow || !textFlow.flowComposer)
return true;
return !textFlow.flowComposer.isDamaged(textFlow.textLength);
}
private function spellCheckRange(start:uint, end:uint):void {
//hh.preSpellCheckRange(start, end);
hh.clearSquiggles();
if ( isHaloComponent ) {
//if (end <= start) return;
var firstLine:int = mTextField.getLineIndexOfChar(start);
var rect:Rectangle = mTextField.getCharBoundaries(start);
var counter:uint = start;
var numLines:Number = 0;
/* mTextField.getCharBoundaries returns null for blank lines and for end of line characters. Placing this workaround
to count line heights until a non-null bounding rectangle is found */
while (rect == null) {
if (++counter > end) {
rect = new Rectangle(0,0,0,0);
break;
}
numLines += mTextField.getLineMetrics(firstLine).height;
firstLine++;
rect = mTextField.getCharBoundaries(counter);
}
var yoffset:Number = rect.y - numLines;
var pleft:uint = (mTextField.parent as UIComponent).getStyle("paddingLeft");
var ptop:uint = (mTextField.parent as UIComponent).getStyle("paddingTop");
var offsetPoint:Point = new Point(pleft, ptop-yoffset);
hh.offsetPoint = offsetPoint;
}
var tokenizer:TextTokenizer = new TextTokenizer(mTextField.text.substring(start,end));
for ( var token:Token = tokenizer.getFirstToken(); token != tokenizer.getLastToken(); token= tokenizer.getNextToken(token) ) {
var result:Boolean=_spellingService.checkWord(mTextField.text.substring(token.first+start, token.last+start));
if (!result){
if (_checkLastWord || (token.last+start != mTextField.text.length))
//hh.highlightWord(token.first+start, token.last+start-1);
//tokens.push(new Token(token.first+start, token.last+start-1));
hh.drawSquiggleAt(new Token(token.first+start, token.last+start-1));
}
}
//hh.postSpellCheckRange(start, end);
//hh.offsetPoint = offsetPoint;
//hh.drawSquiggles(tokens);
}
private function getValidFirstWordIndex():int{
var index:int;
if ( mTextField is TextField ) {
index = mTextField.getLineOffset(mTextField.scrollV-1);
}else if (mTextField is RichEditableText ) {
// Check for computeSelectionIndexInContainer which throws when lineindex == 0
try {
index = SelectionManager.computeSelectionIndex(mTextField.textFlow, mTextField, mTextField, 0 + mTextField.horizontalScrollPosition, 0 + mTextField.verticalScrollPosition);
} catch (err:Error)
{
//TODO: report error
index = 0;
}
}
return index;
}
private function getValidLastWordIndex():int{
var index:int;
if ( mTextField is TextField ) {
index = mTextField.getLineOffset(mTextField.bottomScrollV-1)+mTextField.getLineLength(mTextField.bottomScrollV-1);
}else if (mTextField is RichEditableText ) {
// Check for computeSelectionIndexInContainer which throws when lineindex == 0
try {
index = SelectionManager.computeSelectionIndex(mTextField.textFlow, mTextField, mTextField, mTextField.width+mTextField.horizontalScrollPosition, mTextField.height+mTextField.verticalScrollPosition);
} catch (err:Error)
{
//TODO: report error
index = 0;
}
}
return index;
}
private function loadConfig():void
{
_resource_locale = SpellingConfiguration.resourceTable.getResource(_dictionaryName);
if ((_resource_locale != null) || (SpellUI._configXML != null))
loadConfigComplete(null);
else {
SpellUI._configXMLLoader.addEventListener(Event.COMPLETE, loadConfigComplete);
if (SpellUI._configXMLLoading == false)
{
SpellUI._configXMLLoader.load(new URLRequest(_spellingConfigUrl));
SpellUI._configXMLLoading = true;
}
}
}
private function loadConfigComplete(evt:Event):void{
if (_resource_locale == null) {
if (SpellUI._configXML == null)
SpellUI._configXML = new XML(evt.target.data);
SpellingConfiguration.resourceTable.setResource(_dictionaryName,{rule:SpellUI._configXML.LanguageResource.(@languageCode==_dictionaryName).@ruleFile,
dict:SpellUI._configXML.LanguageResource.(@languageCode==_dictionaryName).@dictionaryFile});
}
//New Added
_spellingService = createSpellingService(_dictionaryName);
_spellingService.addEventListener(Event.COMPLETE, loadDictComplete);
_spellingService.init();
}
protected function createSpellingService(dictionaryName:String):SpellingService
{
return new SpellingService(dictionaryName);
}
private function loadDictComplete(evt:Event):void
{
//_newchecker = new SpellChecker(_hundict);
// Lazy loading the UD only when the main dict is loaded successfully
if ((SpellUI._cache["Squiggly_UD"] as UserDictionary) == null)
{
_sharedObject = SharedObject.getLocal("Squiggly_v03");
var vec:Vector.<String> = new Vector.<String>();
if (_sharedObject.data.ud) {
for each (var w:String in _sharedObject.data.ud)
vec.push(w);
}
_userDictionary = new UserDictionary(vec);
SpellUI._cache["Squiggly_SO"] = _sharedObject;
SpellUI._cache["Squiggly_UD"] = _userDictionary;
}
else
{
_sharedObject = SpellUI._cache["Squiggly_SO"];
_userDictionary = SpellUI._cache["Squiggly_UD"];
}
_spellingService.addUserDictionary(_userDictionary);
// Add the context menu, this might be not successful
_scm = null;
try {
addContextMenu(null);
}
catch (err:Error)
{
// TODO: error handling here
}
_actualParent.addEventListener(Event.ADDED_TO_STAGE, addContextMenu);
}
private function addContextMenu(event:Event):void
{
if ( _scm != null ) return;
hh = createHighlighter(isHaloComponent, _actualParent);
hw = createWordProcessor(isHaloComponent, _actualParent);
_scm = new SpellingContextMenu(hh, hw, _spellingService, _actualParent, _actualParent.contextMenu);
_scm.setIgnoreWordCallback( addWordToUserDictionary );
// Halo need this
if (_actualParent.contextMenu == null)
{
_actualParent.contextMenu = _scm.contextMenu;
}
_spellingEnabled = true;
try {
doSpellingJob();
}
catch (err:Error)
{
// If it fails here, it should later triggered by the render event, so no need to do anything
}
}
protected function createHighlighter(isHaloComoponent:Boolean, parent:*):IHighlighter
{
return isHaloComoponent ? new HaloHighlighter(parent) : new SparkHighlighter(parent);
}
protected function createWordProcessor(isHaloComoponent:Boolean, parent:*):IWordProcessor
{
return isHaloComoponent ? new HaloWordProcessor(parent) : new SparkWordProcessor( _actualParent);
}
private function addWordToUserDictionary(word:String):void
{
_userDictionary.addWord(word);
// TODO: serialization here might affect ther performance
_sharedObject.data.ud = _userDictionary.wordList;
}
/**
* @private
*/
private function cleanUp():void {
if(hh != null)
{
hh.clearSquiggles();
}
if(_scm != null)
{
_scm.cleanUp();
}
_actualParent.removeEventListener(Event.ADDED_TO_STAGE, addContextMenu);
mTextField.removeEventListener(ScrollEvent.SCROLL, spellCheckScreen);
mTextField.parent.removeEventListener(Event.RENDER, spellCheckScreen);
mTextField.parent.removeEventListener(Event.CHANGE, handleChangeEvent);
mTextField.removeEventListener(FocusEvent.FOCUS_OUT, handleFocusOut);
mTextField.removeEventListener(FocusEvent.FOCUS_IN, handleFocusIn);
}
}
}