//////////////////////////////////////////////////////////////////////////////// // // 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.accessibility { import flash.accessibility.Accessibility; import flash.accessibility.AccessibilityImplementation; import flash.accessibility.AccessibilityProperties; import flash.display.DisplayObject; import flash.events.Event; import flashx.textLayout.edit.EditingMode; import flashx.textLayout.edit.ISelectionManager; import flashx.textLayout.elements.FlowElement; import flashx.textLayout.elements.FlowLeafElement; import flashx.textLayout.elements.GlobalSettings; import flashx.textLayout.elements.ParagraphElement; import flashx.textLayout.elements.TextFlow; import flashx.textLayout.events.CompositionCompleteEvent; import flashx.textLayout.tlf_internal; use namespace tlf_internal; //TODO this is in text_edit... which violates MVC yet again... what to do? //import flashx.textLayout.events.SelectionEvent; //TODO handle selectable text when FP implements the new selection API: // http://frpbugapp.macromedia.com/bugapp/detail.asp?id=217540 // To catch the selection changes reliably, listen for SelectionEvent, // which is dispatched on the TextFlow whenever the selection changes. //TODO handle scrolling? might need to expose scrolling in here //TODO handle hyperlinks? I don't know if MSAA has a concept for this // (what other text advanced features must be accessible? graphics?) //TODO what if there is HTML in it? strip it, or read it? we don't have an // htmlText property, do we? // TODO Do we want to read the contents of each sprite and stop, even if the // text flows into other sprite, meaning we read text in taborder; or do we // want to read the entire model and not worry about the presentation // (simpler)? Not sure if I can get the contents of each sprite separately. // TODO TESTING: // * Test that JAWS reads when setting the focus programmatically. // * Tests for changing every part of the model programmatically -- role // and state should update accordingly, visibility, and text contents. // * Test that setting tabOrder reads as expected. What happens if you set // tabOrder on multiple flowComposers? //TODO update this comment after integration /** * @private * The TextAccImpl class adds accessibility for text components. * This hooks into DisplayObjects when TextFlow.container is set. */ public class TextAccImpl extends AccessibilityImplementation { //TODO might want to put these constants in a new class if they are // used anywhere else. /** Default state */ protected static const STATE_SYSTEM_NORMAL:uint = 0x00000000; /** Read-only text */ protected static const STATE_SYSTEM_READONLY:uint = 0x00000040; /** Inivisible text */ //TODO unused, but supported state for text in MSAA protected static const STATE_SYSTEM_INVISIBLE:uint = 0x00008000; /** Default role -- read-only, unselectable text. */ protected static const ROLE_SYSTEM_STATICTEXT:uint = 0x29; /** Editable OR read-only, selectable text. */ protected static const ROLE_SYSTEM_TEXT:uint = 0x2a; /* When the name changes (name is the text conent in STATICTEXT). */ protected static const EVENT_OBJECT_NAMECHANGE:uint = 0x800c; /* When the value changes (value is the text content in TEXT). */ protected static const EVENT_OBJECT_VALUECHANGE:uint = 0x800e; /** * A reference to the DisplayObject that is hosting accessible text. */ //TODO for now this assumes only the first DO in a flow is accessible // in the future each flow DO should host its own accimpl and read // the text only for its own box. // // Or... perhaps we use getChildIDArray to manage all the text // flows if they are linked below some master component (but I don't // think this is the way it will happen). protected var textContainer:DisplayObject; /** * A reference to the TextFlow where our text originates. */ protected var textFlow:TextFlow; /** * Constructor. * * @param textContainer The DisplayObject instance that this * TextAccImpl instance is making accessible. * @param textFlow The TextFlow that is hosting the textContainer. */ public function TextAccImpl(textCont:DisplayObject, textFlow:TextFlow) { super(); this.textContainer = textCont; this.textFlow = textFlow; // stub is true when you are NOT providing an acc implementation // reports to reader as graphic stub = false; if (textCont.accessibilityProperties == null) { textCont.accessibilityProperties = new AccessibilityProperties(); } //TODO // setup event listeners for text selection and model changes //textFlow.addEventListener(SelectionEvent.SELECTION_CHANGE, // eventHandler); textFlow.addEventListener(CompositionCompleteEvent.COMPOSITION_COMPLETE, eventHandler); } public function detachListeners():void { textFlow.removeEventListener(CompositionCompleteEvent.COMPOSITION_COMPLETE, eventHandler); } /** * Returns the system role for the text. * * @param childID uint. * @return Role associated with the text. */ override public function get_accRole(childID:uint):uint { // trace("get_accRole()"); const iManager:ISelectionManager = textFlow.interactionManager; if (iManager == null) { // non-selectable, non-editable text is STATICTEXT return ROLE_SYSTEM_STATICTEXT } else // iManager is an IEditManager and/or ISelectionManager { // read-only selectable or editable selectable text are TEXT return ROLE_SYSTEM_TEXT; } } /** * Returns the state of the text. * * @param childID uint. * @return Role associated with the text. */ override public function get_accState(childID:uint):uint { // trace("get_accState()"); const iManager:ISelectionManager = textFlow.interactionManager; //TODO handle STATE_SYSTEM_INVISIBLE for all cases below // and add an event to detect changes--does Flash support this? //TODO handle STATE_SYSTEM_PROTECTED for all cases below // if vellum gets a concept of password fields, then it needs to // emit this value if the field is converted to a password; // otherwise the Flex framework will need to be sure to emit // this state in a text input component. // note: focus-related states are handled by the player if (iManager == null) { // non-selectable, non-editable text return STATE_SYSTEM_READONLY; } // must check IEditManager before ISelectionManager (it can be both) else if (iManager.editingMode == EditingMode.READ_WRITE) { // editable selectable text return STATE_SYSTEM_NORMAL; } else // if (iManager instanceof ISelectionManager) { // read-only selectable text return STATE_SYSTEM_READONLY; } } /** * Returns the name of the text. * * @param childID uint. * @return Name of the text. */ override public function get_accName(childID:uint):String { // trace("get_accName()"); switch (get_accRole(childID)) { case ROLE_SYSTEM_STATICTEXT: { //TODO this SHOULD come from TextConverter, but then there is a // circular build dependency since importExport builds // against model, and it probably violates mvc //return TextConverter.export(textFlow, // TextConverter.PLAIN_TEXT_FORMAT); //TODO this is probably expensive. is there a way to cache // this and know when dirty? //TODO look at the generation and determine when it's dirty return exportToString(textFlow); } case ROLE_SYSTEM_TEXT: default: return null; } } /** * Returns the value of the text. * * @param childID uint. * @return Name of the text. */ override public function get_accValue(childID:uint):String { // trace("get_accValue()"); switch (get_accRole(childID)) { case ROLE_SYSTEM_TEXT: { //TODO this SHOULD come from TextConverter, but then there is a // circular build dependency since importExport builds // against model, and it probably violates mvc //return TextConverter.export(textFlow, // TextConverter.PLAIN_TEXT_FORMAT); // TODO this is probably expensive. is there a way to cache // this and know when dirty? //TODO look at the generation and determine when it's dirty return exportToString(textFlow); } case ROLE_SYSTEM_STATICTEXT: default: return null; } } /** * Handles COMPOSITION_COMPLETE and SELECTION_CHANGE events, * updates the MSAA model. */ protected function eventHandler(event:Event):void { switch (event.type) { // This updates the entire accessibility DOM. // get_accName is probably expensive here, it happens ONLY if // JAWS is running, otherwise Accessibility.* calls are NOOP. // // Event does NOT fire when interactionManager changes; ideally // we'd want to tell MSAA the role changed, but apparently roles // are typically static and that's not a supported workflow. // instead, Flash occasionally polls the displaylist, e.g. when // you mouseover. calling updateProperties() doesn't necessarily // trigger role updates (calls to get_acc*()). case CompositionCompleteEvent.COMPOSITION_COMPLETE: { // TODO change childID from 0 if we use getChildIDArray // otherwise delete this comment try { Accessibility.sendEvent(textContainer, 0, EVENT_OBJECT_NAMECHANGE); Accessibility.sendEvent(textContainer, 0, EVENT_OBJECT_VALUECHANGE); Accessibility.updateProperties(); } catch (e_err:Error) { // generic error occurred. // this can happen in the SA player since there is no // Accessibility implementation. } break; } //TODO when we have the FP selection APIs // case SelectionEvent.SELECTION_CHANGE: // { // // this is just stubbed code, I don't know what *needs* to // // be done for SELECTION_CHANGE // Accessibility.sendEvent(textContainer, 0, // EVENT_OBJECT_TEXTSELECTIONCHANGED); // Accessibility.updateProperties(); // break; // } } } /** * TODO HACK, remove and refactor. * * This is copied from PlainTextExportFilter, which I would prefer to * access through TextConverter.export(textFlow, * TextConverter.PLAIN_TEXT_FORMAT); * * But, PTEF is in importExport, which builds against text_model, * which is a circular dependency. * * Also, it seems to be adding a trailing newline, which is bad for * accessibility unless it is really there. * * Might want to export and strip out hyphens. * Move it to the model? */ private static function exportToString(source:TextFlow):String { var leaf:FlowLeafElement = source.getFirstLeaf(); var rslt:String = ""; var curString:String = ""; var discretionaryHyphen:String = String.fromCharCode(0x00AD); while (leaf) { var p:ParagraphElement = leaf.getParagraph(); while (true) { curString = leaf.text; //split out discretionary hyphen and put string back together var temparray:Array = curString.split(discretionaryHyphen); curString = temparray.join(""); rslt += curString; leaf = leaf.getNextLeaf(p); if (!leaf) { // we want newlines between paragraphs but not at the end rslt += "\n"; break; } } leaf = p.getLastLeaf().getNextLeaf(); } return rslt; } /** * The zero-based character index value of the first character in the current selection. * Components which wish to support inline IME or Accessibility should call into this method. * * @return the index of the character at the anchor end of the selection, or -1 if no text is selected. * * @playerversion Flash 10.0 * @langversion 3.0 */ public function get selectionActiveIndex():int { var selMgr:ISelectionManager = textFlow.interactionManager; var selIndex:int = -1; if(selMgr && selMgr.editingMode != EditingMode.READ_ONLY) { selIndex = selMgr.activePosition; } return selIndex; } /** * The zero-based character index value of the last character in the current selection. * Components which wish to support inline IME or Accessibility should call into this method. * * @return the index of the character at the active end of the selection, or -1 if no text is selected. * * @playerversion Flash 10.0 * @langversion 3.0 */ public function get selectionAnchorIndex():int { var selMgr:ISelectionManager = textFlow.interactionManager; var selIndex:int = -1; if(selMgr && selMgr.editingMode != EditingMode.READ_ONLY) { selIndex = selMgr.anchorPosition; } return selIndex; } /** Enable search index for Ichabod * Returns the entire text of the TextFlow, or null if search index is not enabled * @see GlobalSettings.searchIndexEnabled */ public function get searchText():String { return GlobalSettings.enableSearch ? textFlow.getText() : null; } } }