accessible/src/jsat/Presentation.jsm

Wed, 31 Dec 2014 07:16:47 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:16:47 +0100
branch
TOR_BUG_9701
changeset 3
141e0f1194b1
permissions
-rw-r--r--

Revert simplistic fix pending revisit of Mozilla integration attempt.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 'use strict';
     7 const Cc = Components.classes;
     8 const Ci = Components.interfaces;
     9 const Cu = Components.utils;
    10 const Cr = Components.results;
    12 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
    13 XPCOMUtils.defineLazyModuleGetter(this, 'Utils',
    14   'resource://gre/modules/accessibility/Utils.jsm');
    15 XPCOMUtils.defineLazyModuleGetter(this, 'Logger',
    16   'resource://gre/modules/accessibility/Utils.jsm');
    17 XPCOMUtils.defineLazyModuleGetter(this, 'PivotContext',
    18   'resource://gre/modules/accessibility/Utils.jsm');
    19 XPCOMUtils.defineLazyModuleGetter(this, 'UtteranceGenerator',
    20   'resource://gre/modules/accessibility/OutputGenerator.jsm');
    21 XPCOMUtils.defineLazyModuleGetter(this, 'BrailleGenerator',
    22   'resource://gre/modules/accessibility/OutputGenerator.jsm');
    23 XPCOMUtils.defineLazyModuleGetter(this, 'Roles',
    24   'resource://gre/modules/accessibility/Constants.jsm');
    25 XPCOMUtils.defineLazyModuleGetter(this, 'States',
    26   'resource://gre/modules/accessibility/Constants.jsm');
    28 this.EXPORTED_SYMBOLS = ['Presentation'];
    30 /**
    31  * The interface for all presenter classes. A presenter could be, for example,
    32  * a speech output module, or a visual cursor indicator.
    33  */
    34 function Presenter() {}
    36 Presenter.prototype = {
    37   /**
    38    * The type of presenter. Used for matching it with the appropriate output method.
    39    */
    40   type: 'Base',
    42   /**
    43    * The virtual cursor's position changed.
    44    * @param {PivotContext} aContext the context object for the new pivot
    45    *   position.
    46    * @param {int} aReason the reason for the pivot change.
    47    *   See nsIAccessiblePivot.
    48    */
    49   pivotChanged: function pivotChanged(aContext, aReason) {},
    51   /**
    52    * An object's action has been invoked.
    53    * @param {nsIAccessible} aObject the object that has been invoked.
    54    * @param {string} aActionName the name of the action.
    55    */
    56   actionInvoked: function actionInvoked(aObject, aActionName) {},
    58   /**
    59    * Text has changed, either by the user or by the system. TODO.
    60    */
    61   textChanged: function textChanged(aIsInserted, aStartOffset,
    62                                     aLength, aText,
    63                                     aModifiedText) {},
    65   /**
    66    * Text selection has changed. TODO.
    67    */
    68   textSelectionChanged: function textSelectionChanged(aText, aStart, aEnd, aOldStart, aOldEnd, aIsFromUser) {},
    70   /**
    71    * Selection has changed. TODO.
    72    * @param {nsIAccessible} aObject the object that has been selected.
    73    */
    74   selectionChanged: function selectionChanged(aObject) {},
    76   /**
    77    * Value has changed.
    78    * @param {nsIAccessible} aAccessible the object whose value has changed.
    79    */
    80   valueChanged: function valueChanged(aAccessible) {},
    82   /**
    83    * The tab, or the tab's document state has changed.
    84    * @param {nsIAccessible} aDocObj the tab document accessible that has had its
    85    *    state changed, or null if the tab has no associated document yet.
    86    * @param {string} aPageState the state name for the tab, valid states are:
    87    *    'newtab', 'loading', 'newdoc', 'loaded', 'stopped', and 'reload'.
    88    */
    89   tabStateChanged: function tabStateChanged(aDocObj, aPageState) {},
    91   /**
    92    * The current tab has changed.
    93    * @param {PivotContext} aDocContext context object for tab's
    94    *   document.
    95    * @param {PivotContext} aVCContext context object for tab's current
    96    *   virtual cursor position.
    97    */
    98   tabSelected: function tabSelected(aDocContext, aVCContext) {},
   100   /**
   101    * The viewport has changed, either a scroll, pan, zoom, or
   102    *    landscape/portrait toggle.
   103    * @param {Window} aWindow window of viewport that changed.
   104    */
   105   viewportChanged: function viewportChanged(aWindow) {},
   107   /**
   108    * We have entered or left text editing mode.
   109    */
   110   editingModeChanged: function editingModeChanged(aIsEditing) {},
   112   /**
   113    * Announce something. Typically an app state change.
   114    */
   115   announce: function announce(aAnnouncement) {},
   119   /**
   120    * Announce a live region.
   121    * @param  {PivotContext} aContext context object for an accessible.
   122    * @param  {boolean} aIsPolite A politeness level for a live region.
   123    * @param  {boolean} aIsHide An indicator of hide/remove event.
   124    * @param  {string} aModifiedText Optional modified text.
   125    */
   126   liveRegion: function liveRegionShown(aContext, aIsPolite, aIsHide,
   127     aModifiedText) {}
   128 };
   130 /**
   131  * Visual presenter. Draws a box around the virtual cursor's position.
   132  */
   134 this.VisualPresenter = function VisualPresenter() {
   135   this._displayedAccessibles = new WeakMap();
   136 };
   138 VisualPresenter.prototype = {
   139   __proto__: Presenter.prototype,
   141   type: 'Visual',
   143   /**
   144    * The padding in pixels between the object and the highlight border.
   145    */
   146   BORDER_PADDING: 2,
   148   viewportChanged: function VisualPresenter_viewportChanged(aWindow) {
   149     let currentDisplay = this._displayedAccessibles.get(aWindow);
   150     if (!currentDisplay) {
   151       return null;
   152     }
   154     let currentAcc = currentDisplay.accessible;
   155     let start = currentDisplay.startOffset;
   156     let end = currentDisplay.endOffset;
   157     if (Utils.isAliveAndVisible(currentAcc)) {
   158       let bounds = (start === -1 && end === -1) ? Utils.getBounds(currentAcc) :
   159                    Utils.getTextBounds(currentAcc, start, end);
   161       return {
   162         type: this.type,
   163         details: {
   164           method: 'showBounds',
   165           bounds: bounds,
   166           padding: this.BORDER_PADDING
   167         }
   168       };
   169     }
   171     return null;
   172   },
   174   pivotChanged: function VisualPresenter_pivotChanged(aContext, aReason) {
   175     if (!aContext.accessible) {
   176       // XXX: Don't hide because another vc may be using the highlight.
   177       return null;
   178     }
   180     this._displayedAccessibles.set(aContext.accessible.document.window,
   181                                    { accessible: aContext.accessibleForBounds,
   182                                      startOffset: aContext.startOffset,
   183                                      endOffset: aContext.endOffset });
   185     try {
   186       aContext.accessibleForBounds.scrollTo(
   187         Ci.nsIAccessibleScrollType.SCROLL_TYPE_ANYWHERE);
   189       let bounds = (aContext.startOffset === -1 && aContext.endOffset === -1) ?
   190             aContext.bounds : Utils.getTextBounds(aContext.accessibleForBounds,
   191                                                   aContext.startOffset,
   192                                                   aContext.endOffset);
   194       return {
   195         type: this.type,
   196         details: {
   197           method: 'showBounds',
   198           bounds: bounds,
   199           padding: this.BORDER_PADDING
   200         }
   201       };
   202     } catch (e) {
   203       Logger.logException(e, 'Failed to get bounds');
   204       return null;
   205     }
   206   },
   208   tabSelected: function VisualPresenter_tabSelected(aDocContext, aVCContext) {
   209     return this.pivotChanged(aVCContext, Ci.nsIAccessiblePivot.REASON_NONE);
   210   },
   212   tabStateChanged: function VisualPresenter_tabStateChanged(aDocObj,
   213                                                             aPageState) {
   214     if (aPageState == 'newdoc')
   215       return {type: this.type, details: {method: 'hideBounds'}};
   217     return null;
   218   },
   220   announce: function VisualPresenter_announce(aAnnouncement) {
   221     return {
   222       type: this.type,
   223       details: {
   224         method: 'showAnnouncement',
   225         text: aAnnouncement,
   226         duration: 1000
   227       }
   228     };
   229   }
   230 };
   232 /**
   233  * Android presenter. Fires Android a11y events.
   234  */
   236 this.AndroidPresenter = function AndroidPresenter() {};
   238 AndroidPresenter.prototype = {
   239   __proto__: Presenter.prototype,
   241   type: 'Android',
   243   // Android AccessibilityEvent type constants.
   244   ANDROID_VIEW_CLICKED: 0x01,
   245   ANDROID_VIEW_LONG_CLICKED: 0x02,
   246   ANDROID_VIEW_SELECTED: 0x04,
   247   ANDROID_VIEW_FOCUSED: 0x08,
   248   ANDROID_VIEW_TEXT_CHANGED: 0x10,
   249   ANDROID_WINDOW_STATE_CHANGED: 0x20,
   250   ANDROID_VIEW_HOVER_ENTER: 0x80,
   251   ANDROID_VIEW_HOVER_EXIT: 0x100,
   252   ANDROID_VIEW_SCROLLED: 0x1000,
   253   ANDROID_VIEW_TEXT_SELECTION_CHANGED: 0x2000,
   254   ANDROID_ANNOUNCEMENT: 0x4000,
   255   ANDROID_VIEW_ACCESSIBILITY_FOCUSED: 0x8000,
   256   ANDROID_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY: 0x20000,
   258   pivotChanged: function AndroidPresenter_pivotChanged(aContext, aReason) {
   259     if (!aContext.accessible)
   260       return null;
   262     let androidEvents = [];
   264     let isExploreByTouch = (aReason == Ci.nsIAccessiblePivot.REASON_POINT &&
   265                             Utils.AndroidSdkVersion >= 14);
   266     let focusEventType = (Utils.AndroidSdkVersion >= 16) ?
   267       this.ANDROID_VIEW_ACCESSIBILITY_FOCUSED :
   268       this.ANDROID_VIEW_FOCUSED;
   270     if (isExploreByTouch) {
   271       // This isn't really used by TalkBack so this is a half-hearted attempt
   272       // for now.
   273       androidEvents.push({eventType: this.ANDROID_VIEW_HOVER_EXIT, text: []});
   274     }
   276     let brailleOutput = {};
   277     if (Utils.AndroidSdkVersion >= 16) {
   278       if (!this._braillePresenter) {
   279         this._braillePresenter = new BraillePresenter();
   280       }
   281       brailleOutput = this._braillePresenter.pivotChanged(aContext, aReason).
   282                          details;
   283     }
   285     if (aReason === Ci.nsIAccessiblePivot.REASON_TEXT) {
   286       if (Utils.AndroidSdkVersion >= 16) {
   287         let adjustedText = aContext.textAndAdjustedOffsets;
   289         androidEvents.push({
   290           eventType: this.ANDROID_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
   291           text: [adjustedText.text],
   292           fromIndex: adjustedText.startOffset,
   293           toIndex: adjustedText.endOffset
   294         });
   295       }
   296     } else {
   297       let state = Utils.getState(aContext.accessible);
   298       androidEvents.push({eventType: (isExploreByTouch) ?
   299                            this.ANDROID_VIEW_HOVER_ENTER : focusEventType,
   300                          text: UtteranceGenerator.genForContext(aContext).output,
   301                          bounds: aContext.bounds,
   302                          clickable: aContext.accessible.actionCount > 0,
   303                          checkable: state.contains(States.CHECKABLE),
   304                          checked: state.contains(States.CHECKED),
   305                          brailleOutput: brailleOutput});
   306     }
   309     return {
   310       type: this.type,
   311       details: androidEvents
   312     };
   313   },
   315   actionInvoked: function AndroidPresenter_actionInvoked(aObject, aActionName) {
   316     let state = Utils.getState(aObject);
   318     // Checkable objects will have a state changed event we will use instead.
   319     if (state.contains(States.CHECKABLE))
   320       return null;
   322     return {
   323       type: this.type,
   324       details: [{
   325         eventType: this.ANDROID_VIEW_CLICKED,
   326         text: UtteranceGenerator.genForAction(aObject, aActionName),
   327         checked: state.contains(States.CHECKED)
   328       }]
   329     };
   330   },
   332   tabSelected: function AndroidPresenter_tabSelected(aDocContext, aVCContext) {
   333     // Send a pivot change message with the full context utterance for this doc.
   334     return this.pivotChanged(aVCContext, Ci.nsIAccessiblePivot.REASON_NONE);
   335   },
   337   tabStateChanged: function AndroidPresenter_tabStateChanged(aDocObj,
   338                                                              aPageState) {
   339     return this.announce(
   340       UtteranceGenerator.genForTabStateChange(aDocObj, aPageState).join(' '));
   341   },
   343   textChanged: function AndroidPresenter_textChanged(aIsInserted, aStart,
   344                                                      aLength, aText,
   345                                                      aModifiedText) {
   346     let eventDetails = {
   347       eventType: this.ANDROID_VIEW_TEXT_CHANGED,
   348       text: [aText],
   349       fromIndex: aStart,
   350       removedCount: 0,
   351       addedCount: 0
   352     };
   354     if (aIsInserted) {
   355       eventDetails.addedCount = aLength;
   356       eventDetails.beforeText =
   357         aText.substring(0, aStart) + aText.substring(aStart + aLength);
   358     } else {
   359       eventDetails.removedCount = aLength;
   360       eventDetails.beforeText =
   361         aText.substring(0, aStart) + aModifiedText + aText.substring(aStart);
   362     }
   364     return {type: this.type, details: [eventDetails]};
   365   },
   367   textSelectionChanged: function AndroidPresenter_textSelectionChanged(aText, aStart,
   368                                                                        aEnd, aOldStart,
   369                                                                        aOldEnd, aIsFromUser) {
   370     let androidEvents = [];
   372     if (Utils.AndroidSdkVersion >= 14 && !aIsFromUser) {
   373       if (!this._braillePresenter) {
   374         this._braillePresenter = new BraillePresenter();
   375       }
   376       let brailleOutput = this._braillePresenter.textSelectionChanged(aText, aStart, aEnd,
   377                                                                       aOldStart, aOldEnd,
   378                                                                       aIsFromUser).details;
   380       androidEvents.push({
   381         eventType: this.ANDROID_VIEW_TEXT_SELECTION_CHANGED,
   382         text: [aText],
   383         fromIndex: aStart,
   384         toIndex: aEnd,
   385         itemCount: aText.length,
   386         brailleOutput: brailleOutput
   387       });
   388     }
   390     if (Utils.AndroidSdkVersion >= 16 && aIsFromUser) {
   391       let [from, to] = aOldStart < aStart ? [aOldStart, aStart] : [aStart, aOldStart];
   392       androidEvents.push({
   393         eventType: this.ANDROID_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
   394         text: [aText],
   395         fromIndex: from,
   396         toIndex: to
   397       });
   398     }
   400     return {
   401       type: this.type,
   402       details: androidEvents
   403     };
   404   },
   406   viewportChanged: function AndroidPresenter_viewportChanged(aWindow) {
   407     if (Utils.AndroidSdkVersion < 14)
   408       return null;
   410     return {
   411       type: this.type,
   412       details: [{
   413         eventType: this.ANDROID_VIEW_SCROLLED,
   414         text: [],
   415         scrollX: aWindow.scrollX,
   416         scrollY: aWindow.scrollY,
   417         maxScrollX: aWindow.scrollMaxX,
   418         maxScrollY: aWindow.scrollMaxY
   419       }]
   420     };
   421   },
   423   editingModeChanged: function AndroidPresenter_editingModeChanged(aIsEditing) {
   424     return this.announce(
   425       UtteranceGenerator.genForEditingMode(aIsEditing).join(' '));
   426   },
   428   announce: function AndroidPresenter_announce(aAnnouncement) {
   429     return {
   430       type: this.type,
   431       details: [{
   432         eventType: (Utils.AndroidSdkVersion >= 16) ?
   433           this.ANDROID_ANNOUNCEMENT : this.ANDROID_VIEW_TEXT_CHANGED,
   434         text: [aAnnouncement],
   435         addedCount: aAnnouncement.length,
   436         removedCount: 0,
   437         fromIndex: 0
   438       }]
   439     };
   440   },
   442   liveRegion: function AndroidPresenter_liveRegion(aContext, aIsPolite,
   443     aIsHide, aModifiedText) {
   444     return this.announce(
   445       UtteranceGenerator.genForLiveRegion(aContext, aIsHide,
   446         aModifiedText).join(' '));
   447   }
   448 };
   450 /**
   451  * A speech presenter for direct TTS output
   452  */
   454 this.SpeechPresenter = function SpeechPresenter() {};
   456 SpeechPresenter.prototype = {
   457   __proto__: Presenter.prototype,
   459   type: 'Speech',
   461   pivotChanged: function SpeechPresenter_pivotChanged(aContext, aReason) {
   462     if (!aContext.accessible)
   463       return null;
   465     return {
   466       type: this.type,
   467       details: {
   468         actions: [
   469           {method: 'playEarcon',
   470            data: aContext.accessible.role === Roles.KEY ?
   471              'virtual_cursor_key' : 'virtual_cursor_move',
   472            options: {}},
   473           {method: 'speak',
   474             data: UtteranceGenerator.genForContext(aContext).output.join(' '),
   475             options: {enqueue: true}}
   476         ]
   477       }
   478     };
   479   },
   481   valueChanged: function SpeechPresenter_valueChanged(aAccessible) {
   482     return {
   483       type: this.type,
   484       details: {
   485         actions: [
   486           { method: 'speak',
   487             data: aAccessible.value,
   488             options: { enqueue: false } }
   489         ]
   490       }
   491     }
   492   },
   494   actionInvoked: function SpeechPresenter_actionInvoked(aObject, aActionName) {
   495     let actions = [];
   496     if (aActionName === 'click') {
   497       actions.push({method: 'playEarcon',
   498                     data: 'clicked',
   499                     options: {}});
   500     } else {
   501       actions.push({method: 'speak',
   502                     data: UtteranceGenerator.genForAction(aObject, aActionName).join(' '),
   503                     options: {enqueue: false}});
   504     }
   505     return { type: this.type, details: { actions: actions } };
   506   },
   508   liveRegion: function SpeechPresenter_liveRegion(aContext, aIsPolite, aIsHide,
   509     aModifiedText) {
   510     return {
   511       type: this.type,
   512       details: {
   513         actions: [{
   514           method: 'speak',
   515           data: UtteranceGenerator.genForLiveRegion(aContext, aIsHide,
   516             aModifiedText).join(' '),
   517           options: {enqueue: aIsPolite}
   518         }]
   519       }
   520     };
   521   },
   523   announce: function SpeechPresenter_announce(aAnnouncement) {
   524     return {
   525       type: this.type,
   526       details: {
   527         actions: [{
   528           method: 'speak', data: aAnnouncement, options: { enqueue: false }
   529         }]
   530       }
   531     };
   532   }
   533 };
   535 /**
   536  * A haptic presenter
   537  */
   539 this.HapticPresenter = function HapticPresenter() {};
   541 HapticPresenter.prototype = {
   542   __proto__: Presenter.prototype,
   544   type: 'Haptic',
   546   PIVOT_CHANGE_PATTERN: [40],
   548   pivotChanged: function HapticPresenter_pivotChanged(aContext, aReason) {
   549     return { type: this.type, details: { pattern: this.PIVOT_CHANGE_PATTERN } };
   550   }
   551 };
   553 /**
   554  * A braille presenter
   555  */
   557 this.BraillePresenter = function BraillePresenter() {};
   559 BraillePresenter.prototype = {
   560   __proto__: Presenter.prototype,
   562   type: 'Braille',
   564   pivotChanged: function BraillePresenter_pivotChanged(aContext, aReason) {
   565     if (!aContext.accessible) {
   566       return null;
   567     }
   569     let brailleOutput = BrailleGenerator.genForContext(aContext);
   570     brailleOutput.output = brailleOutput.output.join(' ');
   571     brailleOutput.selectionStart = 0;
   572     brailleOutput.selectionEnd = 0;
   574     return { type: this.type, details: brailleOutput };
   575   },
   577   textSelectionChanged: function BraillePresenter_textSelectionChanged(aText, aStart,
   578                                                                        aEnd, aOldStart,
   579                                                                        aOldEnd, aIsFromUser) {
   580     return { type: this.type,
   581              details: { selectionStart: aStart,
   582                         selectionEnd: aEnd } };
   583   },
   586 };
   588 this.Presentation = {
   589   get presenters() {
   590     delete this.presenters;
   591     let presenterMap = {
   592       'mobile/android': [VisualPresenter, AndroidPresenter],
   593       'b2g': [VisualPresenter, SpeechPresenter, HapticPresenter],
   594       'browser': [VisualPresenter, SpeechPresenter, HapticPresenter,
   595                   AndroidPresenter]
   596     };
   597     this.presenters = [new P() for (P of presenterMap[Utils.MozBuildApp])];
   598     return this.presenters;
   599   },
   601   pivotChanged: function Presentation_pivotChanged(aPosition, aOldPosition, aReason,
   602                                                    aStartOffset, aEndOffset) {
   603     let context = new PivotContext(aPosition, aOldPosition, aStartOffset, aEndOffset);
   604     return [p.pivotChanged(context, aReason)
   605               for each (p in this.presenters)];
   606   },
   608   actionInvoked: function Presentation_actionInvoked(aObject, aActionName) {
   609     return [p.actionInvoked(aObject, aActionName)
   610               for each (p in this.presenters)];
   611   },
   613   textChanged: function Presentation_textChanged(aIsInserted, aStartOffset,
   614                                     aLength, aText,
   615                                     aModifiedText) {
   616     return [p.textChanged(aIsInserted, aStartOffset, aLength,
   617                           aText, aModifiedText)
   618               for each (p in this.presenters)];
   619   },
   621   textSelectionChanged: function textSelectionChanged(aText, aStart, aEnd,
   622                                                       aOldStart, aOldEnd,
   623                                                       aIsFromUser) {
   624     return [p.textSelectionChanged(aText, aStart, aEnd,
   625                                    aOldStart, aOldEnd, aIsFromUser)
   626               for each (p in this.presenters)];
   627   },
   629   valueChanged: function valueChanged(aAccessible) {
   630     return [ p.valueChanged(aAccessible) for (p of this.presenters) ];
   631   },
   633   tabStateChanged: function Presentation_tabStateChanged(aDocObj, aPageState) {
   634     return [p.tabStateChanged(aDocObj, aPageState)
   635               for each (p in this.presenters)];
   636   },
   638   viewportChanged: function Presentation_viewportChanged(aWindow) {
   639     return [p.viewportChanged(aWindow)
   640               for each (p in this.presenters)];
   641   },
   643   editingModeChanged: function Presentation_editingModeChanged(aIsEditing) {
   644     return [p.editingModeChanged(aIsEditing)
   645               for each (p in this.presenters)];
   646   },
   648   announce: function Presentation_announce(aAnnouncement) {
   649     // XXX: Typically each presenter uses the UtteranceGenerator,
   650     // but there really isn't a point here.
   651     return [p.announce(UtteranceGenerator.genForAnnouncement(aAnnouncement)[0])
   652               for each (p in this.presenters)];
   653   },
   655   liveRegion: function Presentation_liveRegion(aAccessible, aIsPolite, aIsHide,
   656     aModifiedText) {
   657     let context;
   658     if (!aModifiedText) {
   659       context = new PivotContext(aAccessible, null, -1, -1, true,
   660         aIsHide ? true : false);
   661     }
   662     return [p.liveRegion(context, aIsPolite, aIsHide, aModifiedText) for (
   663       p of this.presenters)];
   664   }
   665 };

mercurial