accessible/src/jsat/AccessFu.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
michael@0 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 'use strict';
michael@0 6
michael@0 7 const Cc = Components.classes;
michael@0 8 const Ci = Components.interfaces;
michael@0 9 const Cu = Components.utils;
michael@0 10 const Cr = Components.results;
michael@0 11
michael@0 12 this.EXPORTED_SYMBOLS = ['AccessFu'];
michael@0 13
michael@0 14 Cu.import('resource://gre/modules/Services.jsm');
michael@0 15
michael@0 16 Cu.import('resource://gre/modules/accessibility/Utils.jsm');
michael@0 17
michael@0 18 const ACCESSFU_DISABLE = 0;
michael@0 19 const ACCESSFU_ENABLE = 1;
michael@0 20 const ACCESSFU_AUTO = 2;
michael@0 21
michael@0 22 const SCREENREADER_SETTING = 'accessibility.screenreader';
michael@0 23
michael@0 24 this.AccessFu = {
michael@0 25 /**
michael@0 26 * Initialize chrome-layer accessibility functionality.
michael@0 27 * If accessibility is enabled on the platform, then a special accessibility
michael@0 28 * mode is started.
michael@0 29 */
michael@0 30 attach: function attach(aWindow) {
michael@0 31 Utils.init(aWindow);
michael@0 32
michael@0 33 try {
michael@0 34 Services.androidBridge.handleGeckoMessage(
michael@0 35 { type: 'Accessibility:Ready' });
michael@0 36 Services.obs.addObserver(this, 'Accessibility:Settings', false);
michael@0 37 } catch (x) {
michael@0 38 // Not on Android
michael@0 39 if (aWindow.navigator.mozSettings) {
michael@0 40 let lock = aWindow.navigator.mozSettings.createLock();
michael@0 41 let req = lock.get(SCREENREADER_SETTING);
michael@0 42 req.addEventListener('success', () => {
michael@0 43 this._systemPref = req.result[SCREENREADER_SETTING];
michael@0 44 this._enableOrDisable();
michael@0 45 });
michael@0 46 aWindow.navigator.mozSettings.addObserver(
michael@0 47 SCREENREADER_SETTING, this.handleEvent.bind(this));
michael@0 48 }
michael@0 49 }
michael@0 50
michael@0 51 this._activatePref = new PrefCache(
michael@0 52 'accessibility.accessfu.activate', this._enableOrDisable.bind(this));
michael@0 53
michael@0 54 this._enableOrDisable();
michael@0 55 },
michael@0 56
michael@0 57 /**
michael@0 58 * Shut down chrome-layer accessibility functionality from the outside.
michael@0 59 */
michael@0 60 detach: function detach() {
michael@0 61 // Avoid disabling twice.
michael@0 62 if (this._enabled) {
michael@0 63 this._disable();
michael@0 64 }
michael@0 65 if (Utils.MozBuildApp === 'mobile/android') {
michael@0 66 Services.obs.removeObserver(this, 'Accessibility:Settings');
michael@0 67 } else if (Utils.win.navigator.mozSettings) {
michael@0 68 Utils.win.navigator.mozSettings.removeObserver(
michael@0 69 SCREENREADER_SETTING, this.handleEvent.bind(this));
michael@0 70 }
michael@0 71 delete this._activatePref;
michael@0 72 Utils.uninit();
michael@0 73 },
michael@0 74
michael@0 75 /**
michael@0 76 * Start AccessFu mode, this primarily means controlling the virtual cursor
michael@0 77 * with arrow keys.
michael@0 78 */
michael@0 79 _enable: function _enable() {
michael@0 80 if (this._enabled)
michael@0 81 return;
michael@0 82 this._enabled = true;
michael@0 83
michael@0 84 Cu.import('resource://gre/modules/accessibility/Utils.jsm');
michael@0 85 Cu.import('resource://gre/modules/accessibility/PointerAdapter.jsm');
michael@0 86 Cu.import('resource://gre/modules/accessibility/Presentation.jsm');
michael@0 87
michael@0 88 Logger.info('Enabled');
michael@0 89
michael@0 90 for each (let mm in Utils.AllMessageManagers) {
michael@0 91 this._addMessageListeners(mm);
michael@0 92 this._loadFrameScript(mm);
michael@0 93 }
michael@0 94
michael@0 95 // Add stylesheet
michael@0 96 let stylesheetURL = 'chrome://global/content/accessibility/AccessFu.css';
michael@0 97 let stylesheet = Utils.win.document.createProcessingInstruction(
michael@0 98 'xml-stylesheet', 'href="' + stylesheetURL + '" type="text/css"');
michael@0 99 Utils.win.document.insertBefore(stylesheet, Utils.win.document.firstChild);
michael@0 100 this.stylesheet = Cu.getWeakReference(stylesheet);
michael@0 101
michael@0 102
michael@0 103 // Populate quicknav modes
michael@0 104 this._quicknavModesPref =
michael@0 105 new PrefCache(
michael@0 106 'accessibility.accessfu.quicknav_modes',
michael@0 107 (aName, aValue) => {
michael@0 108 this.Input.quickNavMode.updateModes(aValue);
michael@0 109 }, true);
michael@0 110
michael@0 111 // Check for output notification
michael@0 112 this._notifyOutputPref =
michael@0 113 new PrefCache('accessibility.accessfu.notify_output');
michael@0 114
michael@0 115
michael@0 116 this.Input.start();
michael@0 117 Output.start();
michael@0 118 PointerAdapter.start();
michael@0 119
michael@0 120 Services.obs.addObserver(this, 'remote-browser-shown', false);
michael@0 121 Services.obs.addObserver(this, 'inprocess-browser-shown', false);
michael@0 122 Services.obs.addObserver(this, 'Accessibility:NextObject', false);
michael@0 123 Services.obs.addObserver(this, 'Accessibility:PreviousObject', false);
michael@0 124 Services.obs.addObserver(this, 'Accessibility:Focus', false);
michael@0 125 Services.obs.addObserver(this, 'Accessibility:ActivateObject', false);
michael@0 126 Services.obs.addObserver(this, 'Accessibility:LongPress', false);
michael@0 127 Services.obs.addObserver(this, 'Accessibility:MoveByGranularity', false);
michael@0 128 Utils.win.addEventListener('TabOpen', this);
michael@0 129 Utils.win.addEventListener('TabClose', this);
michael@0 130 Utils.win.addEventListener('TabSelect', this);
michael@0 131
michael@0 132 if (this.readyCallback) {
michael@0 133 this.readyCallback();
michael@0 134 delete this.readyCallback;
michael@0 135 }
michael@0 136
michael@0 137 if (Utils.MozBuildApp !== 'mobile/android') {
michael@0 138 this.announce(
michael@0 139 Utils.stringBundle.GetStringFromName('screenReaderStarted'));
michael@0 140 }
michael@0 141 },
michael@0 142
michael@0 143 /**
michael@0 144 * Disable AccessFu and return to default interaction mode.
michael@0 145 */
michael@0 146 _disable: function _disable() {
michael@0 147 if (!this._enabled)
michael@0 148 return;
michael@0 149
michael@0 150 this._enabled = false;
michael@0 151
michael@0 152 Logger.info('Disabled');
michael@0 153
michael@0 154 Utils.win.document.removeChild(this.stylesheet.get());
michael@0 155
michael@0 156 if (Utils.MozBuildApp !== 'mobile/android') {
michael@0 157 this.announce(
michael@0 158 Utils.stringBundle.GetStringFromName('screenReaderStopped'));
michael@0 159 }
michael@0 160
michael@0 161 for each (let mm in Utils.AllMessageManagers) {
michael@0 162 mm.sendAsyncMessage('AccessFu:Stop');
michael@0 163 this._removeMessageListeners(mm);
michael@0 164 }
michael@0 165
michael@0 166 this.Input.stop();
michael@0 167 Output.stop();
michael@0 168 PointerAdapter.stop();
michael@0 169
michael@0 170 Utils.win.removeEventListener('TabOpen', this);
michael@0 171 Utils.win.removeEventListener('TabClose', this);
michael@0 172 Utils.win.removeEventListener('TabSelect', this);
michael@0 173
michael@0 174 Services.obs.removeObserver(this, 'remote-browser-shown');
michael@0 175 Services.obs.removeObserver(this, 'inprocess-browser-shown');
michael@0 176 Services.obs.removeObserver(this, 'Accessibility:NextObject');
michael@0 177 Services.obs.removeObserver(this, 'Accessibility:PreviousObject');
michael@0 178 Services.obs.removeObserver(this, 'Accessibility:Focus');
michael@0 179 Services.obs.removeObserver(this, 'Accessibility:ActivateObject');
michael@0 180 Services.obs.removeObserver(this, 'Accessibility:LongPress');
michael@0 181 Services.obs.removeObserver(this, 'Accessibility:MoveByGranularity');
michael@0 182
michael@0 183 delete this._quicknavModesPref;
michael@0 184 delete this._notifyOutputPref;
michael@0 185
michael@0 186 if (this.doneCallback) {
michael@0 187 this.doneCallback();
michael@0 188 delete this.doneCallback;
michael@0 189 }
michael@0 190 },
michael@0 191
michael@0 192 _enableOrDisable: function _enableOrDisable() {
michael@0 193 try {
michael@0 194 if (!this._activatePref) {
michael@0 195 return;
michael@0 196 }
michael@0 197 let activatePref = this._activatePref.value;
michael@0 198 if (activatePref == ACCESSFU_ENABLE ||
michael@0 199 this._systemPref && activatePref == ACCESSFU_AUTO)
michael@0 200 this._enable();
michael@0 201 else
michael@0 202 this._disable();
michael@0 203 } catch (x) {
michael@0 204 dump('Error ' + x.message + ' ' + x.fileName + ':' + x.lineNumber);
michael@0 205 }
michael@0 206 },
michael@0 207
michael@0 208 receiveMessage: function receiveMessage(aMessage) {
michael@0 209 Logger.debug(() => {
michael@0 210 return ['Recieved', aMessage.name, JSON.stringify(aMessage.json)];
michael@0 211 });
michael@0 212
michael@0 213 switch (aMessage.name) {
michael@0 214 case 'AccessFu:Ready':
michael@0 215 let mm = Utils.getMessageManager(aMessage.target);
michael@0 216 if (this._enabled) {
michael@0 217 mm.sendAsyncMessage('AccessFu:Start',
michael@0 218 {method: 'start', buildApp: Utils.MozBuildApp});
michael@0 219 }
michael@0 220 break;
michael@0 221 case 'AccessFu:Present':
michael@0 222 this._output(aMessage.json, aMessage.target);
michael@0 223 break;
michael@0 224 case 'AccessFu:Input':
michael@0 225 this.Input.setEditState(aMessage.json);
michael@0 226 break;
michael@0 227 case 'AccessFu:ActivateContextMenu':
michael@0 228 this.Input.activateContextMenu(aMessage.json);
michael@0 229 break;
michael@0 230 case 'AccessFu:DoScroll':
michael@0 231 this.Input.doScroll(aMessage.json);
michael@0 232 break;
michael@0 233 }
michael@0 234 },
michael@0 235
michael@0 236 _output: function _output(aPresentationData, aBrowser) {
michael@0 237 for each (let presenter in aPresentationData) {
michael@0 238 if (!presenter)
michael@0 239 continue;
michael@0 240
michael@0 241 try {
michael@0 242 Output[presenter.type](presenter.details, aBrowser);
michael@0 243 } catch (x) {
michael@0 244 Logger.logException(x);
michael@0 245 }
michael@0 246 }
michael@0 247
michael@0 248 if (this._notifyOutputPref.value) {
michael@0 249 Services.obs.notifyObservers(null, 'accessfu-output',
michael@0 250 JSON.stringify(aPresentationData));
michael@0 251 }
michael@0 252 },
michael@0 253
michael@0 254 _loadFrameScript: function _loadFrameScript(aMessageManager) {
michael@0 255 if (this._processedMessageManagers.indexOf(aMessageManager) < 0) {
michael@0 256 aMessageManager.loadFrameScript(
michael@0 257 'chrome://global/content/accessibility/content-script.js', true);
michael@0 258 this._processedMessageManagers.push(aMessageManager);
michael@0 259 } else if (this._enabled) {
michael@0 260 // If the content-script is already loaded and AccessFu is enabled,
michael@0 261 // send an AccessFu:Start message.
michael@0 262 aMessageManager.sendAsyncMessage('AccessFu:Start',
michael@0 263 {method: 'start', buildApp: Utils.MozBuildApp});
michael@0 264 }
michael@0 265 },
michael@0 266
michael@0 267 _addMessageListeners: function _addMessageListeners(aMessageManager) {
michael@0 268 aMessageManager.addMessageListener('AccessFu:Present', this);
michael@0 269 aMessageManager.addMessageListener('AccessFu:Input', this);
michael@0 270 aMessageManager.addMessageListener('AccessFu:Ready', this);
michael@0 271 aMessageManager.addMessageListener('AccessFu:ActivateContextMenu', this);
michael@0 272 aMessageManager.addMessageListener('AccessFu:DoScroll', this);
michael@0 273 },
michael@0 274
michael@0 275 _removeMessageListeners: function _removeMessageListeners(aMessageManager) {
michael@0 276 aMessageManager.removeMessageListener('AccessFu:Present', this);
michael@0 277 aMessageManager.removeMessageListener('AccessFu:Input', this);
michael@0 278 aMessageManager.removeMessageListener('AccessFu:Ready', this);
michael@0 279 aMessageManager.removeMessageListener('AccessFu:ActivateContextMenu', this);
michael@0 280 aMessageManager.removeMessageListener('AccessFu:DoScroll', this);
michael@0 281 },
michael@0 282
michael@0 283 _handleMessageManager: function _handleMessageManager(aMessageManager) {
michael@0 284 if (this._enabled) {
michael@0 285 this._addMessageListeners(aMessageManager);
michael@0 286 }
michael@0 287 this._loadFrameScript(aMessageManager);
michael@0 288 },
michael@0 289
michael@0 290 observe: function observe(aSubject, aTopic, aData) {
michael@0 291 switch (aTopic) {
michael@0 292 case 'Accessibility:Settings':
michael@0 293 this._systemPref = JSON.parse(aData).enabled;
michael@0 294 this._enableOrDisable();
michael@0 295 break;
michael@0 296 case 'Accessibility:NextObject':
michael@0 297 this.Input.moveCursor('moveNext', 'Simple', 'gesture');
michael@0 298 break;
michael@0 299 case 'Accessibility:PreviousObject':
michael@0 300 this.Input.moveCursor('movePrevious', 'Simple', 'gesture');
michael@0 301 break;
michael@0 302 case 'Accessibility:ActivateObject':
michael@0 303 this.Input.activateCurrent(JSON.parse(aData));
michael@0 304 break;
michael@0 305 case 'Accessibility:LongPress':
michael@0 306 this.Input.sendContextMenuMessage();
michael@0 307 break;
michael@0 308 case 'Accessibility:Focus':
michael@0 309 this._focused = JSON.parse(aData);
michael@0 310 if (this._focused) {
michael@0 311 this.autoMove({ forcePresent: true, noOpIfOnScreen: true });
michael@0 312 }
michael@0 313 break;
michael@0 314 case 'Accessibility:MoveByGranularity':
michael@0 315 this.Input.moveByGranularity(JSON.parse(aData));
michael@0 316 break;
michael@0 317 case 'remote-browser-shown':
michael@0 318 case 'inprocess-browser-shown':
michael@0 319 {
michael@0 320 // Ignore notifications that aren't from a BrowserOrApp
michael@0 321 let frameLoader = aSubject.QueryInterface(Ci.nsIFrameLoader);
michael@0 322 if (!frameLoader.ownerIsBrowserOrAppFrame) {
michael@0 323 return;
michael@0 324 }
michael@0 325 this._handleMessageManager(frameLoader.messageManager);
michael@0 326 break;
michael@0 327 }
michael@0 328 }
michael@0 329 },
michael@0 330
michael@0 331 handleEvent: function handleEvent(aEvent) {
michael@0 332 switch (aEvent.type) {
michael@0 333 case 'TabOpen':
michael@0 334 {
michael@0 335 let mm = Utils.getMessageManager(aEvent.target);
michael@0 336 this._handleMessageManager(mm);
michael@0 337 break;
michael@0 338 }
michael@0 339 case 'TabClose':
michael@0 340 {
michael@0 341 let mm = Utils.getMessageManager(aEvent.target);
michael@0 342 let mmIndex = this._processedMessageManagers.indexOf(mm);
michael@0 343 if (mmIndex > -1) {
michael@0 344 this._removeMessageListeners(mm);
michael@0 345 this._processedMessageManagers.splice(mmIndex, 1);
michael@0 346 }
michael@0 347 break;
michael@0 348 }
michael@0 349 case 'TabSelect':
michael@0 350 {
michael@0 351 if (this._focused) {
michael@0 352 // We delay this for half a second so the awesomebar could close,
michael@0 353 // and we could use the current coordinates for the content item.
michael@0 354 // XXX TODO figure out how to avoid magic wait here.
michael@0 355 this.autoMove({
michael@0 356 delay: 500,
michael@0 357 forcePresent: true,
michael@0 358 noOpIfOnScreen: true,
michael@0 359 moveMethod: 'moveFirst' });
michael@0 360 }
michael@0 361 break;
michael@0 362 }
michael@0 363 default:
michael@0 364 {
michael@0 365 // A settings change, it does not have an event type
michael@0 366 if (aEvent.settingName == SCREENREADER_SETTING) {
michael@0 367 this._systemPref = aEvent.settingValue;
michael@0 368 this._enableOrDisable();
michael@0 369 }
michael@0 370 break;
michael@0 371 }
michael@0 372 }
michael@0 373 },
michael@0 374
michael@0 375 autoMove: function autoMove(aOptions) {
michael@0 376 let mm = Utils.getMessageManager(Utils.CurrentBrowser);
michael@0 377 mm.sendAsyncMessage('AccessFu:AutoMove', aOptions);
michael@0 378 },
michael@0 379
michael@0 380 announce: function announce(aAnnouncement) {
michael@0 381 this._output(Presentation.announce(aAnnouncement), Utils.CurrentBrowser);
michael@0 382 },
michael@0 383
michael@0 384 // So we don't enable/disable twice
michael@0 385 _enabled: false,
michael@0 386
michael@0 387 // Layerview is focused
michael@0 388 _focused: false,
michael@0 389
michael@0 390 // Keep track of message managers tha already have a 'content-script.js'
michael@0 391 // injected.
michael@0 392 _processedMessageManagers: [],
michael@0 393
michael@0 394 /**
michael@0 395 * Adjusts the given bounds relative to the given browser. Converts from screen
michael@0 396 * or device pixels to either device or CSS pixels.
michael@0 397 * @param {Rect} aJsonBounds the bounds to adjust
michael@0 398 * @param {browser} aBrowser the browser we want the bounds relative to
michael@0 399 * @param {bool} aToCSSPixels whether to convert to CSS pixels (as opposed to
michael@0 400 * device pixels)
michael@0 401 * @param {bool} aFromDevicePixels whether to convert from device pixels (as
michael@0 402 * opposed to screen pixels)
michael@0 403 */
michael@0 404 adjustContentBounds: function(aJsonBounds, aBrowser, aToCSSPixels, aFromDevicePixels) {
michael@0 405 let bounds = new Rect(aJsonBounds.left, aJsonBounds.top,
michael@0 406 aJsonBounds.right - aJsonBounds.left,
michael@0 407 aJsonBounds.bottom - aJsonBounds.top);
michael@0 408 let win = Utils.win;
michael@0 409 let dpr = win.devicePixelRatio;
michael@0 410 let vp = Utils.getViewport(win);
michael@0 411 let offset = { left: -win.mozInnerScreenX, top: -win.mozInnerScreenY };
michael@0 412
michael@0 413 if (!aBrowser.contentWindow) {
michael@0 414 // OOP browser, add offset of browser.
michael@0 415 // The offset of the browser element in relation to its parent window.
michael@0 416 let clientRect = aBrowser.getBoundingClientRect();
michael@0 417 let win = aBrowser.ownerDocument.defaultView;
michael@0 418 offset.left += clientRect.left + win.mozInnerScreenX;
michael@0 419 offset.top += clientRect.top + win.mozInnerScreenY;
michael@0 420 }
michael@0 421
michael@0 422 // Here we scale from screen pixels to layout device pixels by dividing by
michael@0 423 // the resolution (caused by pinch-zooming). The resolution is the viewport
michael@0 424 // zoom divided by the devicePixelRatio. If there's no viewport, then we're
michael@0 425 // on a platform without pinch-zooming and we can just ignore this.
michael@0 426 if (!aFromDevicePixels && vp) {
michael@0 427 bounds = bounds.scale(vp.zoom / dpr, vp.zoom / dpr);
michael@0 428 }
michael@0 429
michael@0 430 // Add the offset; the offset is in CSS pixels, so multiply the
michael@0 431 // devicePixelRatio back in before adding to preserve unit consistency.
michael@0 432 bounds = bounds.translate(offset.left * dpr, offset.top * dpr);
michael@0 433
michael@0 434 // If we want to get to CSS pixels from device pixels, this needs to be
michael@0 435 // further divided by the devicePixelRatio due to widget scaling.
michael@0 436 if (aToCSSPixels) {
michael@0 437 bounds = bounds.scale(1 / dpr, 1 / dpr);
michael@0 438 }
michael@0 439
michael@0 440 return bounds.expandToIntegers();
michael@0 441 }
michael@0 442 };
michael@0 443
michael@0 444 var Output = {
michael@0 445 brailleState: {
michael@0 446 startOffset: 0,
michael@0 447 endOffset: 0,
michael@0 448 text: '',
michael@0 449 selectionStart: 0,
michael@0 450 selectionEnd: 0,
michael@0 451
michael@0 452 init: function init(aOutput) {
michael@0 453 if (aOutput && 'output' in aOutput) {
michael@0 454 this.startOffset = aOutput.startOffset;
michael@0 455 this.endOffset = aOutput.endOffset;
michael@0 456 // We need to append a space at the end so that the routing key corresponding
michael@0 457 // to the end of the output (i.e. the space) can be hit to move the caret there.
michael@0 458 this.text = aOutput.output + ' ';
michael@0 459 this.selectionStart = typeof aOutput.selectionStart === 'number' ?
michael@0 460 aOutput.selectionStart : this.selectionStart;
michael@0 461 this.selectionEnd = typeof aOutput.selectionEnd === 'number' ?
michael@0 462 aOutput.selectionEnd : this.selectionEnd;
michael@0 463
michael@0 464 return { text: this.text,
michael@0 465 selectionStart: this.selectionStart,
michael@0 466 selectionEnd: this.selectionEnd };
michael@0 467 }
michael@0 468
michael@0 469 return null;
michael@0 470 },
michael@0 471
michael@0 472 adjustText: function adjustText(aText) {
michael@0 473 let newBraille = [];
michael@0 474 let braille = {};
michael@0 475
michael@0 476 let prefix = this.text.substring(0, this.startOffset).trim();
michael@0 477 if (prefix) {
michael@0 478 prefix += ' ';
michael@0 479 newBraille.push(prefix);
michael@0 480 }
michael@0 481
michael@0 482 newBraille.push(aText);
michael@0 483
michael@0 484 let suffix = this.text.substring(this.endOffset).trim();
michael@0 485 if (suffix) {
michael@0 486 suffix = ' ' + suffix;
michael@0 487 newBraille.push(suffix);
michael@0 488 }
michael@0 489
michael@0 490 this.startOffset = braille.startOffset = prefix.length;
michael@0 491 this.text = braille.text = newBraille.join('') + ' ';
michael@0 492 this.endOffset = braille.endOffset = braille.text.length - suffix.length;
michael@0 493 braille.selectionStart = this.selectionStart;
michael@0 494 braille.selectionEnd = this.selectionEnd;
michael@0 495
michael@0 496 return braille;
michael@0 497 },
michael@0 498
michael@0 499 adjustSelection: function adjustSelection(aSelection) {
michael@0 500 let braille = {};
michael@0 501
michael@0 502 braille.startOffset = this.startOffset;
michael@0 503 braille.endOffset = this.endOffset;
michael@0 504 braille.text = this.text;
michael@0 505 this.selectionStart = braille.selectionStart = aSelection.selectionStart + this.startOffset;
michael@0 506 this.selectionEnd = braille.selectionEnd = aSelection.selectionEnd + this.startOffset;
michael@0 507
michael@0 508 return braille;
michael@0 509 }
michael@0 510 },
michael@0 511
michael@0 512 speechHelper: {
michael@0 513 EARCONS: ['virtual_cursor_move.ogg',
michael@0 514 'virtual_cursor_key.ogg',
michael@0 515 'clicked.ogg'],
michael@0 516
michael@0 517 earconBuffers: {},
michael@0 518
michael@0 519 inited: false,
michael@0 520
michael@0 521 webspeechEnabled: false,
michael@0 522
michael@0 523 deferredOutputs: [],
michael@0 524
michael@0 525 init: function init() {
michael@0 526 let window = Utils.win;
michael@0 527 this.webspeechEnabled = !!window.speechSynthesis &&
michael@0 528 !!window.SpeechSynthesisUtterance;
michael@0 529
michael@0 530 let settingsToGet = 2;
michael@0 531 let settingsCallback = (aName, aSetting) => {
michael@0 532 if (--settingsToGet > 0) {
michael@0 533 return;
michael@0 534 }
michael@0 535
michael@0 536 this.inited = true;
michael@0 537
michael@0 538 for (let actions of this.deferredOutputs) {
michael@0 539 this.output(actions);
michael@0 540 }
michael@0 541 };
michael@0 542
michael@0 543 this._volumeSetting = new SettingCache(
michael@0 544 'accessibility.screenreader-volume', settingsCallback,
michael@0 545 { defaultValue: 1, callbackNow: true, callbackOnce: true });
michael@0 546 this._rateSetting = new SettingCache(
michael@0 547 'accessibility.screenreader-rate', settingsCallback,
michael@0 548 { defaultValue: 0, callbackNow: true, callbackOnce: true });
michael@0 549
michael@0 550 for (let earcon of this.EARCONS) {
michael@0 551 let earconName = /(^.*)\..*$/.exec(earcon)[1];
michael@0 552 this.earconBuffers[earconName] = new WeakMap();
michael@0 553 this.earconBuffers[earconName].set(
michael@0 554 window, new window.Audio('chrome://global/content/accessibility/' + earcon));
michael@0 555 }
michael@0 556 },
michael@0 557
michael@0 558 uninit: function uninit() {
michael@0 559 if (this.inited) {
michael@0 560 delete this._volumeSetting;
michael@0 561 delete this._rateSetting;
michael@0 562 }
michael@0 563 this.inited = false;
michael@0 564 },
michael@0 565
michael@0 566 output: function output(aActions) {
michael@0 567 if (!this.inited) {
michael@0 568 this.deferredOutputs.push(aActions);
michael@0 569 return;
michael@0 570 }
michael@0 571
michael@0 572 for (let action of aActions) {
michael@0 573 let window = Utils.win;
michael@0 574 Logger.debug('tts.' + action.method, '"' + action.data + '"',
michael@0 575 JSON.stringify(action.options));
michael@0 576
michael@0 577 if (!action.options.enqueue && this.webspeechEnabled) {
michael@0 578 window.speechSynthesis.cancel();
michael@0 579 }
michael@0 580
michael@0 581 if (action.method === 'speak' && this.webspeechEnabled) {
michael@0 582 let utterance = new window.SpeechSynthesisUtterance(action.data);
michael@0 583 let requestedRate = this._rateSetting.value;
michael@0 584 utterance.volume = this._volumeSetting.value;
michael@0 585 utterance.rate = requestedRate >= 0 ?
michael@0 586 requestedRate + 1 : 1 / (Math.abs(requestedRate) + 1);
michael@0 587 window.speechSynthesis.speak(utterance);
michael@0 588 } else if (action.method === 'playEarcon') {
michael@0 589 let audioBufferWeakMap = this.earconBuffers[action.data];
michael@0 590 if (audioBufferWeakMap) {
michael@0 591 let node = audioBufferWeakMap.get(window).cloneNode(false);
michael@0 592 node.volume = this._volumeSetting.value;
michael@0 593 node.play();
michael@0 594 }
michael@0 595 }
michael@0 596 }
michael@0 597 }
michael@0 598 },
michael@0 599
michael@0 600 start: function start() {
michael@0 601 Cu.import('resource://gre/modules/Geometry.jsm');
michael@0 602 this.speechHelper.init();
michael@0 603 },
michael@0 604
michael@0 605 stop: function stop() {
michael@0 606 if (this.highlightBox) {
michael@0 607 Utils.win.document.documentElement.removeChild(this.highlightBox.get());
michael@0 608 delete this.highlightBox;
michael@0 609 }
michael@0 610
michael@0 611 if (this.announceBox) {
michael@0 612 Utils.win.document.documentElement.removeChild(this.announceBox.get());
michael@0 613 delete this.announceBox;
michael@0 614 }
michael@0 615
michael@0 616 this.speechHelper.uninit();
michael@0 617 },
michael@0 618
michael@0 619 Speech: function Speech(aDetails, aBrowser) {
michael@0 620 this.speechHelper.output(aDetails.actions);
michael@0 621 },
michael@0 622
michael@0 623 Visual: function Visual(aDetails, aBrowser) {
michael@0 624 switch (aDetails.method) {
michael@0 625 case 'showBounds':
michael@0 626 {
michael@0 627 let highlightBox = null;
michael@0 628 if (!this.highlightBox) {
michael@0 629 // Add highlight box
michael@0 630 highlightBox = Utils.win.document.
michael@0 631 createElementNS('http://www.w3.org/1999/xhtml', 'div');
michael@0 632 Utils.win.document.documentElement.appendChild(highlightBox);
michael@0 633 highlightBox.id = 'virtual-cursor-box';
michael@0 634
michael@0 635 // Add highlight inset for inner shadow
michael@0 636 let inset = Utils.win.document.
michael@0 637 createElementNS('http://www.w3.org/1999/xhtml', 'div');
michael@0 638 inset.id = 'virtual-cursor-inset';
michael@0 639
michael@0 640 highlightBox.appendChild(inset);
michael@0 641 this.highlightBox = Cu.getWeakReference(highlightBox);
michael@0 642 } else {
michael@0 643 highlightBox = this.highlightBox.get();
michael@0 644 }
michael@0 645
michael@0 646 let padding = aDetails.padding;
michael@0 647 let r = AccessFu.adjustContentBounds(aDetails.bounds, aBrowser, true);
michael@0 648
michael@0 649 // First hide it to avoid flickering when changing the style.
michael@0 650 highlightBox.style.display = 'none';
michael@0 651 highlightBox.style.top = (r.top - padding) + 'px';
michael@0 652 highlightBox.style.left = (r.left - padding) + 'px';
michael@0 653 highlightBox.style.width = (r.width + padding*2) + 'px';
michael@0 654 highlightBox.style.height = (r.height + padding*2) + 'px';
michael@0 655 highlightBox.style.display = 'block';
michael@0 656
michael@0 657 break;
michael@0 658 }
michael@0 659 case 'hideBounds':
michael@0 660 {
michael@0 661 let highlightBox = this.highlightBox ? this.highlightBox.get() : null;
michael@0 662 if (highlightBox)
michael@0 663 highlightBox.style.display = 'none';
michael@0 664 break;
michael@0 665 }
michael@0 666 case 'showAnnouncement':
michael@0 667 {
michael@0 668 let announceBox = this.announceBox ? this.announceBox.get() : null;
michael@0 669 if (!announceBox) {
michael@0 670 announceBox = Utils.win.document.
michael@0 671 createElementNS('http://www.w3.org/1999/xhtml', 'div');
michael@0 672 announceBox.id = 'announce-box';
michael@0 673 Utils.win.document.documentElement.appendChild(announceBox);
michael@0 674 this.announceBox = Cu.getWeakReference(announceBox);
michael@0 675 }
michael@0 676
michael@0 677 announceBox.innerHTML = '<div>' + aDetails.text + '</div>';
michael@0 678 announceBox.classList.add('showing');
michael@0 679
michael@0 680 if (this._announceHideTimeout)
michael@0 681 Utils.win.clearTimeout(this._announceHideTimeout);
michael@0 682
michael@0 683 if (aDetails.duration > 0)
michael@0 684 this._announceHideTimeout = Utils.win.setTimeout(
michael@0 685 function () {
michael@0 686 announceBox.classList.remove('showing');
michael@0 687 this._announceHideTimeout = 0;
michael@0 688 }.bind(this), aDetails.duration);
michael@0 689 break;
michael@0 690 }
michael@0 691 case 'hideAnnouncement':
michael@0 692 {
michael@0 693 let announceBox = this.announceBox ? this.announceBox.get() : null;
michael@0 694 if (announceBox)
michael@0 695 announceBox.classList.remove('showing');
michael@0 696 break;
michael@0 697 }
michael@0 698 }
michael@0 699 },
michael@0 700
michael@0 701 get androidBridge() {
michael@0 702 delete this.androidBridge;
michael@0 703 if (Utils.MozBuildApp === 'mobile/android') {
michael@0 704 this.androidBridge = Services.androidBridge;
michael@0 705 } else {
michael@0 706 this.androidBridge = null;
michael@0 707 }
michael@0 708 return this.androidBridge;
michael@0 709 },
michael@0 710
michael@0 711 Android: function Android(aDetails, aBrowser) {
michael@0 712 const ANDROID_VIEW_TEXT_CHANGED = 0x10;
michael@0 713 const ANDROID_VIEW_TEXT_SELECTION_CHANGED = 0x2000;
michael@0 714
michael@0 715 if (!this.androidBridge) {
michael@0 716 return;
michael@0 717 }
michael@0 718
michael@0 719 for each (let androidEvent in aDetails) {
michael@0 720 androidEvent.type = 'Accessibility:Event';
michael@0 721 if (androidEvent.bounds)
michael@0 722 androidEvent.bounds = AccessFu.adjustContentBounds(androidEvent.bounds, aBrowser);
michael@0 723
michael@0 724 switch(androidEvent.eventType) {
michael@0 725 case ANDROID_VIEW_TEXT_CHANGED:
michael@0 726 androidEvent.brailleOutput = this.brailleState.adjustText(androidEvent.text);
michael@0 727 break;
michael@0 728 case ANDROID_VIEW_TEXT_SELECTION_CHANGED:
michael@0 729 androidEvent.brailleOutput = this.brailleState.adjustSelection(androidEvent.brailleOutput);
michael@0 730 break;
michael@0 731 default:
michael@0 732 androidEvent.brailleOutput = this.brailleState.init(androidEvent.brailleOutput);
michael@0 733 break;
michael@0 734 }
michael@0 735 this.androidBridge.handleGeckoMessage(androidEvent);
michael@0 736 }
michael@0 737 },
michael@0 738
michael@0 739 Haptic: function Haptic(aDetails, aBrowser) {
michael@0 740 Utils.win.navigator.vibrate(aDetails.pattern);
michael@0 741 },
michael@0 742
michael@0 743 Braille: function Braille(aDetails, aBrowser) {
michael@0 744 Logger.debug('Braille output: ' + aDetails.text);
michael@0 745 }
michael@0 746 };
michael@0 747
michael@0 748 var Input = {
michael@0 749 editState: {},
michael@0 750
michael@0 751 start: function start() {
michael@0 752 // XXX: This is too disruptive on desktop for now.
michael@0 753 // Might need to add special modifiers.
michael@0 754 if (Utils.MozBuildApp != 'browser') {
michael@0 755 Utils.win.document.addEventListener('keypress', this, true);
michael@0 756 }
michael@0 757 Utils.win.addEventListener('mozAccessFuGesture', this, true);
michael@0 758 },
michael@0 759
michael@0 760 stop: function stop() {
michael@0 761 if (Utils.MozBuildApp != 'browser') {
michael@0 762 Utils.win.document.removeEventListener('keypress', this, true);
michael@0 763 }
michael@0 764 Utils.win.removeEventListener('mozAccessFuGesture', this, true);
michael@0 765 },
michael@0 766
michael@0 767 handleEvent: function Input_handleEvent(aEvent) {
michael@0 768 try {
michael@0 769 switch (aEvent.type) {
michael@0 770 case 'keypress':
michael@0 771 this._handleKeypress(aEvent);
michael@0 772 break;
michael@0 773 case 'mozAccessFuGesture':
michael@0 774 this._handleGesture(aEvent.detail);
michael@0 775 break;
michael@0 776 }
michael@0 777 } catch (x) {
michael@0 778 Logger.logException(x);
michael@0 779 }
michael@0 780 },
michael@0 781
michael@0 782 _handleGesture: function _handleGesture(aGesture) {
michael@0 783 let gestureName = aGesture.type + aGesture.touches.length;
michael@0 784 Logger.debug('Gesture', aGesture.type,
michael@0 785 '(fingers: ' + aGesture.touches.length + ')');
michael@0 786
michael@0 787 switch (gestureName) {
michael@0 788 case 'dwell1':
michael@0 789 case 'explore1':
michael@0 790 this.moveToPoint('Simple', aGesture.touches[0].x,
michael@0 791 aGesture.touches[0].y);
michael@0 792 break;
michael@0 793 case 'doubletap1':
michael@0 794 this.activateCurrent();
michael@0 795 break;
michael@0 796 case 'doubletaphold1':
michael@0 797 this.sendContextMenuMessage();
michael@0 798 break;
michael@0 799 case 'swiperight1':
michael@0 800 this.moveCursor('moveNext', 'Simple', 'gestures');
michael@0 801 break;
michael@0 802 case 'swipeleft1':
michael@0 803 this.moveCursor('movePrevious', 'Simple', 'gesture');
michael@0 804 break;
michael@0 805 case 'swipeup1':
michael@0 806 this.contextAction('backward');
michael@0 807 break;
michael@0 808 case 'swipedown1':
michael@0 809 this.contextAction('forward');
michael@0 810 break;
michael@0 811 case 'exploreend1':
michael@0 812 case 'dwellend1':
michael@0 813 this.activateCurrent(null, true);
michael@0 814 break;
michael@0 815 case 'swiperight2':
michael@0 816 this.sendScrollMessage(-1, true);
michael@0 817 break;
michael@0 818 case 'swipedown2':
michael@0 819 this.sendScrollMessage(-1);
michael@0 820 break;
michael@0 821 case 'swipeleft2':
michael@0 822 this.sendScrollMessage(1, true);
michael@0 823 break;
michael@0 824 case 'swipeup2':
michael@0 825 this.sendScrollMessage(1);
michael@0 826 break;
michael@0 827 case 'explore2':
michael@0 828 Utils.CurrentBrowser.contentWindow.scrollBy(
michael@0 829 -aGesture.deltaX, -aGesture.deltaY);
michael@0 830 break;
michael@0 831 case 'swiperight3':
michael@0 832 this.moveCursor('moveNext', this.quickNavMode.current, 'gesture');
michael@0 833 break;
michael@0 834 case 'swipeleft3':
michael@0 835 this.moveCursor('movePrevious', this.quickNavMode.current, 'gesture');
michael@0 836 break;
michael@0 837 case 'swipedown3':
michael@0 838 this.quickNavMode.next();
michael@0 839 AccessFu.announce('quicknav_' + this.quickNavMode.current);
michael@0 840 break;
michael@0 841 case 'swipeup3':
michael@0 842 this.quickNavMode.previous();
michael@0 843 AccessFu.announce('quicknav_' + this.quickNavMode.current);
michael@0 844 break;
michael@0 845 }
michael@0 846 },
michael@0 847
michael@0 848 _handleKeypress: function _handleKeypress(aEvent) {
michael@0 849 let target = aEvent.target;
michael@0 850
michael@0 851 // Ignore keys with modifiers so the content could take advantage of them.
michael@0 852 if (aEvent.ctrlKey || aEvent.altKey || aEvent.metaKey)
michael@0 853 return;
michael@0 854
michael@0 855 switch (aEvent.keyCode) {
michael@0 856 case 0:
michael@0 857 // an alphanumeric key was pressed, handle it separately.
michael@0 858 // If it was pressed with either alt or ctrl, just pass through.
michael@0 859 // If it was pressed with meta, pass the key on without the meta.
michael@0 860 if (this.editState.editing)
michael@0 861 return;
michael@0 862
michael@0 863 let key = String.fromCharCode(aEvent.charCode);
michael@0 864 try {
michael@0 865 let [methodName, rule] = this.keyMap[key];
michael@0 866 this.moveCursor(methodName, rule, 'keyboard');
michael@0 867 } catch (x) {
michael@0 868 return;
michael@0 869 }
michael@0 870 break;
michael@0 871 case aEvent.DOM_VK_RIGHT:
michael@0 872 if (this.editState.editing) {
michael@0 873 if (!this.editState.atEnd)
michael@0 874 // Don't move forward if caret is not at end of entry.
michael@0 875 // XXX: Fix for rtl
michael@0 876 return;
michael@0 877 else
michael@0 878 target.blur();
michael@0 879 }
michael@0 880 this.moveCursor(aEvent.shiftKey ? 'moveLast' : 'moveNext', 'Simple', 'keyboard');
michael@0 881 break;
michael@0 882 case aEvent.DOM_VK_LEFT:
michael@0 883 if (this.editState.editing) {
michael@0 884 if (!this.editState.atStart)
michael@0 885 // Don't move backward if caret is not at start of entry.
michael@0 886 // XXX: Fix for rtl
michael@0 887 return;
michael@0 888 else
michael@0 889 target.blur();
michael@0 890 }
michael@0 891 this.moveCursor(aEvent.shiftKey ? 'moveFirst' : 'movePrevious', 'Simple', 'keyboard');
michael@0 892 break;
michael@0 893 case aEvent.DOM_VK_UP:
michael@0 894 if (this.editState.multiline) {
michael@0 895 if (!this.editState.atStart)
michael@0 896 // Don't blur content if caret is not at start of text area.
michael@0 897 return;
michael@0 898 else
michael@0 899 target.blur();
michael@0 900 }
michael@0 901
michael@0 902 if (Utils.MozBuildApp == 'mobile/android')
michael@0 903 // Return focus to native Android browser chrome.
michael@0 904 Services.androidBridge.handleGeckoMessage(
michael@0 905 { type: 'ToggleChrome:Focus' });
michael@0 906 break;
michael@0 907 case aEvent.DOM_VK_RETURN:
michael@0 908 if (this.editState.editing)
michael@0 909 return;
michael@0 910 this.activateCurrent();
michael@0 911 break;
michael@0 912 default:
michael@0 913 return;
michael@0 914 }
michael@0 915
michael@0 916 aEvent.preventDefault();
michael@0 917 aEvent.stopPropagation();
michael@0 918 },
michael@0 919
michael@0 920 moveToPoint: function moveToPoint(aRule, aX, aY) {
michael@0 921 let mm = Utils.getMessageManager(Utils.CurrentBrowser);
michael@0 922 mm.sendAsyncMessage('AccessFu:MoveToPoint', {rule: aRule,
michael@0 923 x: aX, y: aY,
michael@0 924 origin: 'top'});
michael@0 925 },
michael@0 926
michael@0 927 moveCursor: function moveCursor(aAction, aRule, aInputType) {
michael@0 928 let mm = Utils.getMessageManager(Utils.CurrentBrowser);
michael@0 929 mm.sendAsyncMessage('AccessFu:MoveCursor',
michael@0 930 {action: aAction, rule: aRule,
michael@0 931 origin: 'top', inputType: aInputType});
michael@0 932 },
michael@0 933
michael@0 934 contextAction: function contextAction(aDirection) {
michael@0 935 // XXX: For now, the only supported context action is adjusting a range.
michael@0 936 let mm = Utils.getMessageManager(Utils.CurrentBrowser);
michael@0 937 mm.sendAsyncMessage('AccessFu:AdjustRange', {direction: aDirection});
michael@0 938 },
michael@0 939
michael@0 940 moveByGranularity: function moveByGranularity(aDetails) {
michael@0 941 const MOVEMENT_GRANULARITY_PARAGRAPH = 8;
michael@0 942
michael@0 943 if (!this.editState.editing) {
michael@0 944 if (aDetails.granularity === MOVEMENT_GRANULARITY_PARAGRAPH) {
michael@0 945 this.moveCursor('move' + aDetails.direction, 'Paragraph', 'gesture');
michael@0 946 return;
michael@0 947 }
michael@0 948 } else {
michael@0 949 aDetails.atStart = this.editState.atStart;
michael@0 950 aDetails.atEnd = this.editState.atEnd;
michael@0 951 }
michael@0 952
michael@0 953 let mm = Utils.getMessageManager(Utils.CurrentBrowser);
michael@0 954 let type = this.editState.editing ? 'AccessFu:MoveCaret' :
michael@0 955 'AccessFu:MoveByGranularity';
michael@0 956 mm.sendAsyncMessage(type, aDetails);
michael@0 957 },
michael@0 958
michael@0 959 activateCurrent: function activateCurrent(aData, aActivateIfKey = false) {
michael@0 960 let mm = Utils.getMessageManager(Utils.CurrentBrowser);
michael@0 961 let offset = aData && typeof aData.keyIndex === 'number' ?
michael@0 962 aData.keyIndex - Output.brailleState.startOffset : -1;
michael@0 963
michael@0 964 mm.sendAsyncMessage('AccessFu:Activate',
michael@0 965 {offset: offset, activateIfKey: aActivateIfKey});
michael@0 966 },
michael@0 967
michael@0 968 sendContextMenuMessage: function sendContextMenuMessage() {
michael@0 969 let mm = Utils.getMessageManager(Utils.CurrentBrowser);
michael@0 970 mm.sendAsyncMessage('AccessFu:ContextMenu', {});
michael@0 971 },
michael@0 972
michael@0 973 activateContextMenu: function activateContextMenu(aDetails) {
michael@0 974 if (Utils.MozBuildApp === 'mobile/android') {
michael@0 975 let p = AccessFu.adjustContentBounds(aDetails.bounds, Utils.CurrentBrowser,
michael@0 976 true, true).center();
michael@0 977 Services.obs.notifyObservers(null, 'Gesture:LongPress',
michael@0 978 JSON.stringify({x: p.x, y: p.y}));
michael@0 979 }
michael@0 980 },
michael@0 981
michael@0 982 setEditState: function setEditState(aEditState) {
michael@0 983 this.editState = aEditState;
michael@0 984 },
michael@0 985
michael@0 986 // XXX: This is here for backwards compatability with screen reader simulator
michael@0 987 // it should be removed when the extension is updated on amo.
michael@0 988 scroll: function scroll(aPage, aHorizontal) {
michael@0 989 this.sendScrollMessage(aPage, aHorizontal);
michael@0 990 },
michael@0 991
michael@0 992 sendScrollMessage: function sendScrollMessage(aPage, aHorizontal) {
michael@0 993 let mm = Utils.getMessageManager(Utils.CurrentBrowser);
michael@0 994 mm.sendAsyncMessage('AccessFu:Scroll', {page: aPage, horizontal: aHorizontal, origin: 'top'});
michael@0 995 },
michael@0 996
michael@0 997 doScroll: function doScroll(aDetails) {
michael@0 998 let horizontal = aDetails.horizontal;
michael@0 999 let page = aDetails.page;
michael@0 1000 let p = AccessFu.adjustContentBounds(aDetails.bounds, Utils.CurrentBrowser,
michael@0 1001 true, true).center();
michael@0 1002 let wu = Utils.win.QueryInterface(Ci.nsIInterfaceRequestor).
michael@0 1003 getInterface(Ci.nsIDOMWindowUtils);
michael@0 1004 wu.sendWheelEvent(p.x, p.y,
michael@0 1005 horizontal ? page : 0, horizontal ? 0 : page, 0,
michael@0 1006 Utils.win.WheelEvent.DOM_DELTA_PAGE, 0, 0, 0, 0);
michael@0 1007 },
michael@0 1008
michael@0 1009 get keyMap() {
michael@0 1010 delete this.keyMap;
michael@0 1011 this.keyMap = {
michael@0 1012 a: ['moveNext', 'Anchor'],
michael@0 1013 A: ['movePrevious', 'Anchor'],
michael@0 1014 b: ['moveNext', 'Button'],
michael@0 1015 B: ['movePrevious', 'Button'],
michael@0 1016 c: ['moveNext', 'Combobox'],
michael@0 1017 C: ['movePrevious', 'Combobox'],
michael@0 1018 d: ['moveNext', 'Landmark'],
michael@0 1019 D: ['movePrevious', 'Landmark'],
michael@0 1020 e: ['moveNext', 'Entry'],
michael@0 1021 E: ['movePrevious', 'Entry'],
michael@0 1022 f: ['moveNext', 'FormElement'],
michael@0 1023 F: ['movePrevious', 'FormElement'],
michael@0 1024 g: ['moveNext', 'Graphic'],
michael@0 1025 G: ['movePrevious', 'Graphic'],
michael@0 1026 h: ['moveNext', 'Heading'],
michael@0 1027 H: ['movePrevious', 'Heading'],
michael@0 1028 i: ['moveNext', 'ListItem'],
michael@0 1029 I: ['movePrevious', 'ListItem'],
michael@0 1030 k: ['moveNext', 'Link'],
michael@0 1031 K: ['movePrevious', 'Link'],
michael@0 1032 l: ['moveNext', 'List'],
michael@0 1033 L: ['movePrevious', 'List'],
michael@0 1034 p: ['moveNext', 'PageTab'],
michael@0 1035 P: ['movePrevious', 'PageTab'],
michael@0 1036 r: ['moveNext', 'RadioButton'],
michael@0 1037 R: ['movePrevious', 'RadioButton'],
michael@0 1038 s: ['moveNext', 'Separator'],
michael@0 1039 S: ['movePrevious', 'Separator'],
michael@0 1040 t: ['moveNext', 'Table'],
michael@0 1041 T: ['movePrevious', 'Table'],
michael@0 1042 x: ['moveNext', 'Checkbox'],
michael@0 1043 X: ['movePrevious', 'Checkbox']
michael@0 1044 };
michael@0 1045
michael@0 1046 return this.keyMap;
michael@0 1047 },
michael@0 1048
michael@0 1049 quickNavMode: {
michael@0 1050 get current() {
michael@0 1051 return this.modes[this._currentIndex];
michael@0 1052 },
michael@0 1053
michael@0 1054 previous: function quickNavMode_previous() {
michael@0 1055 if (--this._currentIndex < 0)
michael@0 1056 this._currentIndex = this.modes.length - 1;
michael@0 1057 },
michael@0 1058
michael@0 1059 next: function quickNavMode_next() {
michael@0 1060 if (++this._currentIndex >= this.modes.length)
michael@0 1061 this._currentIndex = 0;
michael@0 1062 },
michael@0 1063
michael@0 1064 updateModes: function updateModes(aModes) {
michael@0 1065 if (aModes) {
michael@0 1066 this.modes = aModes.split(',');
michael@0 1067 } else {
michael@0 1068 this.modes = [];
michael@0 1069 }
michael@0 1070 },
michael@0 1071
michael@0 1072 _currentIndex: -1
michael@0 1073 }
michael@0 1074 };
michael@0 1075 AccessFu.Input = Input;

mercurial