accessibility_api_handler.js revision cedac228d2dd51db4b79ea1e72c7f249408ee061
158537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)// Copyright 2014 The Chromium Authors. All rights reserved. 258537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)// Use of this source code is governed by a BSD-style license that can be 358537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)// found in the LICENSE file. 458537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) 558537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)/** 658537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) * @fileoverview Accesses Chrome's accessibility extension API and gives 758537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) * spoken feedback for events that happen in the "Chrome of Chrome". 858537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) * 958537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) */ 1058537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) 1158537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)goog.provide('cvox.AccessibilityApiHandler'); 1258537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) 1358537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)goog.require('cvox.AbstractEarcons'); 1458537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)goog.require('cvox.AbstractTts'); 1558537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)goog.require('cvox.BrailleInterface'); 1658537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)goog.require('cvox.BrailleUtil'); 1758537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)goog.require('cvox.ChromeVoxEditableTextBase'); 1858537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)goog.require('cvox.NavBraille'); 19f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) 2058537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) 2158537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)/** 2258537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) * The chrome.experimental.accessibility API is moving to 2358537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) * chrome.accessibilityPrivate, so provide an alias during the transition. 2458537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) * 2558537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) * TODO(dmazzoni): Remove after the stable version of Chrome no longer 2658537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) * has the experimental accessibility API. 2758537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) */ 2858537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)if (!chrome.accessibilityPrivate) 2958537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) chrome.accessibilityPrivate = chrome.experimental.accessibility; 3058537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) 3158537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) 3258537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)/** 3358537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) * Class that adds listeners and handles events from the accessibility API. 3458537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) * @constructor 3558537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) * @implements {cvox.TtsCapturingEventListener} 3658537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) * @param {cvox.TtsInterface} tts The TTS to use for speaking. 3758537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) * @param {cvox.BrailleInterface} braille The braille interface to use for 3858537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) * brailing. 3958537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) * @param {Object} earcons The earcons object to use for playing 4058537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) * earcons. 4158537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) */ 4258537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)cvox.AccessibilityApiHandler = function(tts, braille, earcons) { 4358537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) this.tts = tts; 4458537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) this.braille = braille; 4558537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) this.earcons = earcons; 4658537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) /** 4758537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) * Tracks the previous description received. 4858537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) * @type {Object} 4958537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) * @private 5058537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) */ 5158537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) this.prevDescription_ = {}; 5258537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) try { 5358537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) chrome.accessibilityPrivate.setAccessibilityEnabled(true); 5458537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) chrome.accessibilityPrivate.setNativeAccessibilityEnabled( 5558537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) !cvox.ChromeVox.isActive); 5658537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) this.addEventListeners_(); 5758537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) if (cvox.ChromeVox.isActive) { 5858537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) this.queueAlertsForActiveTab(); 5958537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) } 6058537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) } catch (err) { 6158537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) console.log('Error trying to access accessibility extension api.'); 6258537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) } 6358537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)}; 6458537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) 6558537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)/** 6658537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) * The interface used to manage speech. 6758537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) * @type {cvox.TtsInterface} 68 */ 69cvox.AccessibilityApiHandler.prototype.tts = null; 70 71/** 72 * The interface used to manage braille. 73 * @type {cvox.BrailleInterface} 74 */ 75cvox.AccessibilityApiHandler.prototype.braille = null; 76 77/** 78 * The object used to manage arcons. 79 * @type Object 80 */ 81cvox.AccessibilityApiHandler.prototype.earcons = null; 82 83/** 84 * The object that can describe changes and cursor movement in a generic 85 * editable text field. 86 * @type {Object} 87 */ 88cvox.AccessibilityApiHandler.prototype.editableTextHandler = null; 89 90/** 91 * The name of the editable text field associated with 92 * |editableTextHandler|, so we can tell when focus moves. 93 * @type {string} 94 */ 95cvox.AccessibilityApiHandler.prototype.editableTextName = ''; 96 97/** 98 * The queue mode for the next focus event. 99 * @type {number} 100 */ 101cvox.AccessibilityApiHandler.prototype.nextQueueMode = 0; 102 103/** 104 * The timeout id for the pending text changed event - the return 105 * value from window.setTimeout. We need to delay text events slightly 106 * and return only the last one because sometimes we get a rapid 107 * succession of related events that should all be considered one 108 * bulk change - in particular, autocomplete in the location bar comes 109 * as multiple events in a row. 110 * @type {?number} 111 */ 112cvox.AccessibilityApiHandler.prototype.textChangeTimeout = null; 113 114/** 115 * Most controls have a "context" - the name of the window, dialog, toolbar, 116 * or menu they're contained in. We announce a context once, when you 117 * first enter it - and we don't announce it again when you move to something 118 * else within the same context. This variable keeps track of the most 119 * recent context. 120 * @type {?string} 121 */ 122cvox.AccessibilityApiHandler.prototype.lastContext = null; 123 124/** 125 * Delay in ms between when a text event is received and when it's spoken. 126 * @type {number} 127 * @const 128 */ 129cvox.AccessibilityApiHandler.prototype.TEXT_CHANGE_DELAY = 10; 130 131/** 132 * ID returned from setTimeout to queue up speech on idle. 133 * @type {?number} 134 * @private 135 */ 136cvox.AccessibilityApiHandler.prototype.idleSpeechTimeout_ = null; 137 138/** 139 * Array of strings to speak the next time TTS is idle. 140 * @type {!Array.<string>} 141 * @private 142 */ 143cvox.AccessibilityApiHandler.prototype.idleSpeechQueue_ = []; 144 145/** 146 * Milliseconds of silence to wait before considering speech to be idle. 147 * @const 148 */ 149cvox.AccessibilityApiHandler.prototype.IDLE_SPEECH_DELAY_MS = 500; 150 151/** 152 * Called to let us know that the last speech came from web, and not from 153 * native UI. Clear the context and any state associated with the last 154 * focused control. 155 */ 156cvox.AccessibilityApiHandler.prototype.setWebContext = function(control) { 157 // This will never be spoken - it's just supposed to be a string that 158 // won't match the context of the next control that gets focused. 159 this.lastContext = '--internal-web--'; 160 this.editableTextHandler = null; 161 this.editableTextName = ''; 162} 163 164/** 165 * Adds event listeners. 166 */ 167cvox.AccessibilityApiHandler.prototype.addEventListeners_ = function() { 168 /** Alias getMsg as msg. */ 169 var msg = goog.bind(cvox.ChromeVox.msgs.getMsg, cvox.ChromeVox.msgs); 170 171 var accessibility = chrome.accessibilityPrivate; 172 173 chrome.tabs.onCreated.addListener(goog.bind(function(tab) { 174 if (!cvox.ChromeVox.isActive) { 175 return; 176 } 177 this.tts.speak(msg('chrome_tab_created'), 178 0, 179 cvox.AbstractTts.PERSONALITY_ANNOUNCEMENT); 180 this.braille.write(cvox.NavBraille.fromText(msg('chrome_tab_created'))); 181 this.earcons.playEarcon(cvox.AbstractEarcons.OBJECT_OPEN); 182 }, this)); 183 184 chrome.tabs.onRemoved.addListener(goog.bind(function(tab) { 185 if (!cvox.ChromeVox.isActive) { 186 return; 187 } 188 this.earcons.playEarcon(cvox.AbstractEarcons.OBJECT_CLOSE); 189 }, this)); 190 191 chrome.tabs.onActivated.addListener(goog.bind(function(activeInfo) { 192 if (!cvox.ChromeVox.isActive) { 193 return; 194 } 195 chrome.tabs.get(activeInfo.tabId, goog.bind(function(tab) { 196 if (tab.status == 'loading') { 197 return; 198 } 199 var title = tab.title ? tab.title : tab.url; 200 this.tts.speak(msg('chrome_tab_selected', 201 [title]), 202 cvox.AbstractTts.QUEUE_MODE_FLUSH, 203 cvox.AbstractTts.PERSONALITY_ANNOUNCEMENT); 204 this.braille.write( 205 cvox.NavBraille.fromText(msg('chrome_tab_selected', [title]))); 206 this.earcons.playEarcon(cvox.AbstractEarcons.OBJECT_SELECT); 207 this.queueAlertsForActiveTab(); 208 }, this)); 209 }, this)); 210 211 chrome.tabs.onUpdated.addListener(goog.bind(function(tabId, selectInfo) { 212 if (!cvox.ChromeVox.isActive) { 213 return; 214 } 215 chrome.tabs.get(tabId, goog.bind(function(tab) { 216 if (!tab.active) { 217 return; 218 } 219 if (tab.status == 'loading') { 220 this.earcons.playEarcon(cvox.AbstractEarcons.BUSY_PROGRESS_LOOP); 221 } else { 222 this.earcons.playEarcon(cvox.AbstractEarcons.TASK_SUCCESS); 223 } 224 }, this)); 225 }, this)); 226 227 chrome.windows.onFocusChanged.addListener(goog.bind(function(windowId) { 228 if (!cvox.ChromeVox.isActive) { 229 return; 230 } 231 if (windowId == chrome.windows.WINDOW_ID_NONE) { 232 return; 233 } 234 chrome.windows.get(windowId, goog.bind(function(window) { 235 chrome.tabs.getSelected(windowId, goog.bind(function(tab) { 236 var msg_id = window.incognito ? 'chrome_incognito_window_selected' : 237 'chrome_normal_window_selected'; 238 var title = tab.title ? tab.title : tab.url; 239 this.tts.speak(msg(msg_id, [title]), 240 cvox.AbstractTts.QUEUE_MODE_FLUSH, 241 cvox.AbstractTts.PERSONALITY_ANNOUNCEMENT); 242 this.braille.write(cvox.NavBraille.fromText(msg(msg_id, [title]))); 243 this.earcons.playEarcon(cvox.AbstractEarcons.OBJECT_SELECT); 244 }, this)); 245 }, this)); 246 }, this)); 247 248 chrome.accessibilityPrivate.onWindowOpened.addListener( 249 goog.bind(function(win) { 250 if (!cvox.ChromeVox.isActive) { 251 return; 252 } 253 this.tts.speak(win.name, 254 cvox.AbstractTts.QUEUE_MODE_FLUSH, 255 cvox.AbstractTts.PERSONALITY_ANNOUNCEMENT); 256 this.braille.write(cvox.NavBraille.fromText(win.name)); 257 // Queue the next utterance because a window opening is always followed 258 // by a focus event. 259 this.nextQueueMode = 1; 260 this.earcons.playEarcon(cvox.AbstractEarcons.OBJECT_OPEN); 261 this.queueAlertsForActiveTab(); 262 }, this)); 263 264 chrome.accessibilityPrivate.onWindowClosed.addListener( 265 goog.bind(function(win) { 266 if (!cvox.ChromeVox.isActive) { 267 return; 268 } 269 // Don't speak, just play the earcon. 270 this.earcons.playEarcon(cvox.AbstractEarcons.OBJECT_CLOSE); 271 }, this)); 272 273 chrome.accessibilityPrivate.onMenuOpened.addListener( 274 goog.bind(function(menu) { 275 if (!cvox.ChromeVox.isActive) { 276 return; 277 } 278 this.tts.speak(msg('chrome_menu_opened', [menu.name]), 279 cvox.AbstractTts.QUEUE_MODE_FLUSH, 280 cvox.AbstractTts.PERSONALITY_ANNOUNCEMENT); 281 this.braille.write( 282 cvox.NavBraille.fromText(msg('chrome_menu_opened', [menu.name]))); 283 this.earcons.playEarcon(cvox.AbstractEarcons.OBJECT_OPEN); 284 }, this)); 285 286 chrome.accessibilityPrivate.onMenuClosed.addListener( 287 goog.bind(function(menu) { 288 if (!cvox.ChromeVox.isActive) { 289 return; 290 } 291 // Don't speak, just play the earcon. 292 this.earcons.playEarcon(cvox.AbstractEarcons.OBJECT_CLOSE); 293 }, this)); 294 295 // systemPrivate API is only available when this extension is loaded as a 296 // component extension embedded in Chrome. 297 chrome.permissions.contains( 298 { permissions: ['systemPrivate'] }, 299 goog.bind(function(result) { 300 if (!result) { 301 return; 302 } 303 304 // TODO(plundblad): Remove when the native sound is turned on by default. 305 // See crbug.com:225886. 306 var addOnVolumeChangedListener = goog.bind(function() { 307 chrome.systemPrivate.onVolumeChanged.addListener(goog.bind( 308 function(volume) { 309 if (!cvox.ChromeVox.isActive) { 310 return; 311 } 312 // Don't speak, just play the earcon. 313 this.earcons.playEarcon(cvox.AbstractEarcons.TASK_SUCCESS); 314 }, this)); 315 }, this); 316 if (chrome.commandLinePrivate) { 317 chrome.commandLinePrivate.hasSwitch('disable-volume-adjust-sound', 318 goog.bind(function(result) { 319 if (result) { 320 addOnVolumeChangedListener(); 321 } 322 }, this)); 323 } else { 324 addOnVolumeChangedListener(); 325 } 326 327 chrome.systemPrivate.onBrightnessChanged.addListener( 328 goog.bind( 329 /** 330 * @param {{brightness: number, userInitiated: boolean}} brightness 331 */ 332 function(brightness) { 333 if (brightness.userInitiated) { 334 this.earcons.playEarcon(cvox.AbstractEarcons.TASK_SUCCESS); 335 this.tts.speak( 336 msg('chrome_brightness_changed', [brightness.brightness]), 337 cvox.AbstractTts.QUEUE_MODE_FLUSH, 338 cvox.AbstractTts.PERSONALITY_ANNOUNCEMENT); 339 this.braille.write(cvox.NavBraille.fromText( 340 msg('chrome_brightness_changed', [brightness.brightness]))); 341 } 342 }, this)); 343 344 chrome.systemPrivate.onScreenUnlocked.addListener(goog.bind(function() { 345 chrome.systemPrivate.getUpdateStatus(goog.bind(function(status) { 346 if (!cvox.ChromeVox.isActive) { 347 return; 348 } 349 // Speak about system update when it's ready, otherwise speak nothing. 350 if (status.state == 'NeedRestart') { 351 this.tts.speak(msg('chrome_system_need_restart'), 352 cvox.AbstractTts.QUEUE_MODE_FLUSH, 353 cvox.AbstractTts.PERSONALITY_ANNOUNCEMENT); 354 this.braille.write( 355 cvox.NavBraille.fromText(msg('chrome_system_need_restart'))); 356 } 357 }, this)); 358 }, this)); 359 360 chrome.systemPrivate.onWokeUp.addListener(goog.bind(function() { 361 if (!cvox.ChromeVox.isActive) { 362 return; 363 } 364 // Don't speak, just play the earcon. 365 this.earcons.playEarcon(cvox.AbstractEarcons.OBJECT_OPEN); 366 }, this)); 367 }, this)); 368 369 chrome.accessibilityPrivate.onControlFocused.addListener( 370 goog.bind(this.onControlFocused, this)); 371 372 chrome.accessibilityPrivate.onControlAction.addListener( 373 goog.bind(function(ctl) { 374 if (!cvox.ChromeVox.isActive) { 375 return; 376 } 377 378 var description = this.describe(ctl, true); 379 this.tts.speak(description.utterance, 380 cvox.AbstractTts.QUEUE_MODE_FLUSH, 381 description.ttsProps); 382 description.braille.write(); 383 if (description.earcon) { 384 this.earcons.playEarcon(description.earcon); 385 } 386 }, this)); 387 388 chrome.accessibilityPrivate.onTextChanged.addListener( 389 goog.bind(function(ctl) { 390 if (!cvox.ChromeVox.isActive) { 391 return; 392 } 393 394 if (!this.editableTextHandler || 395 this.editableTextName != ctl.name || 396 this.lastContext != ctl.context) { 397 // Chrome won't send a text change event on a control that isn't 398 // focused. If we get a text change event and it doesn't match the 399 // focused control, treat it as a focus event initially. 400 this.onControlFocused(ctl); 401 return; 402 } 403 404 // Only send the most recent text changed event - throw away anything 405 // that was pending. 406 if (this.textChangeTimeout) { 407 window.clearTimeout(this.textChangeTimeout); 408 } 409 410 // Handle the text change event after a small delay, so multiple 411 // events in rapid succession are handled as a single change. This is 412 // specifically for the location bar with autocomplete - typing a 413 // character and getting the autocompleted text and getting that 414 // text selected may be three separate events. 415 this.textChangeTimeout = window.setTimeout( 416 goog.bind(function() { 417 var textChangeEvent = new cvox.TextChangeEvent( 418 ctl.details.value, 419 ctl.details.selectionStart, 420 ctl.details.selectionEnd, 421 true); // triggered by user 422 this.editableTextHandler.changed( 423 textChangeEvent); 424 this.describe(ctl, false).braille.write(); 425 }, this), this.TEXT_CHANGE_DELAY); 426 }, this)); 427 428 this.tts.addCapturingEventListener(this); 429}; 430 431/** 432 * Handle the feedback when a new control gets focus. 433 * @param {AccessibilityObject} ctl The focused control. 434 */ 435cvox.AccessibilityApiHandler.prototype.onControlFocused = function(ctl) { 436 if (!cvox.ChromeVox.isActive) { 437 return; 438 } 439 440 // Call this first because it may clear this.editableTextHandler. 441 var description = this.describe(ctl, false); 442 443 if (ctl.type == 'textbox') { 444 var start = ctl.details.selectionStart; 445 var end = ctl.details.selectionEnd; 446 if (start > end) { 447 start = ctl.details.selectionEnd; 448 end = ctl.details.selectionStart; 449 } 450 this.editableTextName = ctl.name; 451 this.editableTextHandler = 452 new cvox.ChromeVoxEditableTextBase( 453 ctl.details.value, 454 start, 455 end, 456 ctl.details.isPassword, 457 this.tts); 458 } else { 459 this.editableTextHandler = null; 460 } 461 462 this.tts.speak(description.utterance, 463 this.nextQueueMode, 464 description.ttsProps); 465 description.braille.write(); 466 this.nextQueueMode = 0; 467 if (description.earcon) { 468 this.earcons.playEarcon(description.earcon); 469 } 470}; 471 472/** 473 * Called when any speech starts. 474 */ 475cvox.AccessibilityApiHandler.prototype.onTtsStart = function() { 476 if (this.idleSpeechTimeout_) { 477 window.clearTimeout(this.idleSpeechTimeout_); 478 } 479}; 480 481/** 482 * Called when any speech ends. 483 */ 484cvox.AccessibilityApiHandler.prototype.onTtsEnd = function() { 485 if (this.idleSpeechQueue_.length > 0) { 486 this.idleSpeechTimeout_ = window.setTimeout( 487 goog.bind(this.onTtsIdle, this), 488 this.IDLE_SPEECH_DELAY_MS); 489 } 490}; 491 492/** 493 * Called when speech has been idle for a certain minimum delay. 494 * Speaks queued messages. 495 */ 496cvox.AccessibilityApiHandler.prototype.onTtsIdle = function() { 497 if (this.idleSpeechQueue_.length == 0) { 498 return; 499 } 500 var utterance = this.idleSpeechQueue_.shift(); 501 var msg = goog.bind(cvox.ChromeVox.msgs.getMsg, cvox.ChromeVox.msgs); 502 this.tts.speak(utterance, 503 cvox.AbstractTts.QUEUE_MODE_FLUSH, 504 cvox.AbstractTts.PERSONALITY_ANNOUNCEMENT); 505}; 506 507/** 508 * Given a control received from the accessibility api, determine an 509 * utterance to speak, text to braille, and an earcon to play to describe it. 510 * @param {Object} control The control that had an action performed on it. 511 * @param {boolean} isSelect True if the action is a select action, 512 * otherwise it's a focus action. 513 * @return {Object} An object containing a string field |utterance|, object 514 * |ttsProps|, |braille|, and earcon |earcon|. 515 */ 516cvox.AccessibilityApiHandler.prototype.describe = function(control, isSelect) { 517 /** Alias getMsg as msg. */ 518 var msg = goog.bind(cvox.ChromeVox.msgs.getMsg, cvox.ChromeVox.msgs); 519 520 var s = ''; 521 var braille = {}; 522 var ttsProps = cvox.AbstractTts.PERSONALITY_ANNOUNCEMENT; 523 524 var context = control.context; 525 if (context && context != this.lastContext) { 526 s += context + ', '; 527 this.lastContext = context; 528 this.editableTextHandler = null; 529 } 530 531 var earcon = undefined; 532 var name = control.name.replace(/[_&]+/g, '').replace('...', ''); 533 braille.name = control.name; 534 switch (control.type) { 535 case 'checkbox': 536 braille.roleMsg = 'input_type_checkbox'; 537 if (control.details.isChecked) { 538 earcon = cvox.AbstractEarcons.CHECK_ON; 539 s += msg('describe_checkbox_checked', [name]); 540 braille.state = msg('checkbox_checked_state_brl'); 541 } else { 542 earcon = cvox.AbstractEarcons.CHECK_OFF; 543 s += msg('describe_checkbox_unchecked', [name]); 544 braille.state = msg('checkbox_unchecked_state_brl'); 545 } 546 break; 547 case 'radiobutton': 548 s += name; 549 braille.roleMsg = 'input_type_radio'; 550 if (control.details.isChecked) { 551 earcon = cvox.AbstractEarcons.CHECK_ON; 552 s += msg('describe_radio_selected', [name]); 553 braille.state = msg('radio_selected_state_brl'); 554 } else { 555 earcon = cvox.AbstractEarcons.CHECK_OFF; 556 s += msg('describe_radio_unselected', [name]); 557 braille.state = msg('radio_unselected_state_brl'); 558 } 559 break; 560 case 'menu': 561 s += msg('describe_menu', [name]); 562 braille.roleMsg = 'aria_role_menu'; 563 break; 564 case 'menuitem': 565 s += msg( 566 control.details.hasSubmenu ? 567 'describe_menu_item_with_submenu' : 'describe_menu_item', [name]); 568 braille.roleMsg = 'aria_role_menuitem'; 569 if (control.details.hasSubmenu) { 570 braille.state = msg('aria_has_submenu_brl'); 571 } 572 break; 573 case 'window': 574 s += msg('describe_window', [name]); 575 // No specialization for braille. 576 braille.name = s; 577 break; 578 case 'alert': 579 earcon = cvox.AbstractEarcons.ALERT_NONMODAL; 580 s += msg('aria_role_alert') + ': ' + name; 581 ttsProps = cvox.AbstractTts.PERSONALITY_SYSTEM_ALERT; 582 braille.roleMsg = 'aria_role_alert'; 583 isSelect = false; 584 break; 585 case 'textbox': 586 earcon = cvox.AbstractEarcons.EDITABLE_TEXT; 587 var unnamed = name == '' ? 'unnamed_' : ''; 588 var type, value; 589 if (control.details.isPassword) { 590 type = 'password'; 591 braille.roleMsg = 'input_type_password'; 592 value = control.details.value.replace(/./g, '*'); 593 } else { 594 type = 'textbox'; 595 braille.roleMsg = 'input_type_text'; 596 value = control.details.value; 597 } 598 s += msg('describe_' + unnamed + type, [value, name]); 599 braille.value = cvox.BrailleUtil.createValue( 600 value, control.details.selectionStart, control.details.selectionEnd); 601 break; 602 case 'button': 603 earcon = cvox.AbstractEarcons.BUTTON; 604 s += msg('describe_button', [name]); 605 braille.roleMsg = 'tag_button'; 606 break; 607 case 'combobox': 608 case 'listbox': 609 earcon = cvox.AbstractEarcons.LISTBOX; 610 var unnamed = name == '' ? 'unnamed_' : ''; 611 s += msg('describe_' + unnamed + control.type, 612 [control.details.value, name]); 613 braille.roleMsg = 'tag_select'; 614 break; 615 case 'link': 616 earcon = cvox.AbstractEarcons.LINK; 617 s += msg('describe_link', [name]); 618 braille.roleMsg = 'tag_link'; 619 break; 620 case 'tab': 621 s += msg('describe_tab', [name]); 622 braille.roleMsg = 'aria_role_tab'; 623 break; 624 case 'slider': 625 s += msg('describe_slider', [control.details.stringValue, name]); 626 braille.value = cvox.BrailleUtil.createValue(control.details.stringValue); 627 braille.roleMsg = 'aria_role_slider'; 628 break; 629 case 'treeitem': 630 if (this.prevDescription_ && 631 this.prevDescription_.details && 632 goog.isDef(control.details.itemDepth) && 633 this.prevDescription_.details.itemDepth != 634 control.details.itemDepth) { 635 s += msg('describe_depth', [control.details.itemDepth]); 636 } 637 s += name + ' ' + msg('aria_role_treeitem'); 638 s += control.details.isItemExpanded ? 639 msg('aria_expanded_true') : msg('aria_expanded_false'); 640 641 braille.name = Array(control.details.itemDepth).join(' ') + braille.name; 642 braille.roleMsg = 'aria_role_treeitem'; 643 braille.state = control.details.isItemExpanded ? 644 msg('aria_expanded_true_brl') : msg('aria_expanded_false_brl'); 645 break; 646 647 default: 648 s += name + ', ' + control.type; 649 braille.role = control.type; 650 } 651 652 if (isSelect && control.type != 'slider') { 653 s += msg('describe_selected'); 654 } 655 if (control.details && control.details.itemCount >= 0) { 656 s += msg('describe_index', 657 [control.details.itemIndex + 1, control.details.itemCount]); 658 braille.state = braille.state ? braille.state + ' ' : ''; 659 braille.state += msg('LIST_POSITION_BRL', 660 [control.details.itemIndex + 1, control.details.itemCount]); 661 } 662 663 var description = {}; 664 description.utterance = s; 665 description.ttsProps = ttsProps; 666 var spannable = cvox.BrailleUtil.getTemplated(null, null, braille); 667 var valueSelectionSpan = spannable.getSpanInstanceOf( 668 cvox.BrailleUtil.ValueSelectionSpan); 669 var brailleObj = {text: spannable}; 670 if (valueSelectionSpan) { 671 brailleObj.startIndex = spannable.getSpanStart(valueSelectionSpan); 672 brailleObj.endIndex = spannable.getSpanEnd(valueSelectionSpan); 673 } 674 description.braille = new cvox.NavBraille(brailleObj); 675 description.earcon = earcon; 676 this.prevDescription_ = control; 677 return description; 678}; 679 680/** 681 * Queues alerts for the active tab, if any, which will be spoken 682 * as soon as speech is idle. 683 */ 684cvox.AccessibilityApiHandler.prototype.queueAlertsForActiveTab = function() { 685 this.idleSpeechQueue_.length = 0; 686 var msg = goog.bind(cvox.ChromeVox.msgs.getMsg, cvox.ChromeVox.msgs); 687 688 chrome.tabs.query({'active': true, 'currentWindow': true}, 689 goog.bind(function(tabs) { 690 if (tabs.length < 1) { 691 return; 692 } 693 chrome.accessibilityPrivate.getAlertsForTab( 694 tabs[0].id, goog.bind(function(alerts) { 695 if (alerts.length == 0) { 696 return; 697 } 698 699 var utterance = ''; 700 701 if (alerts.length == 1) { 702 utterance += msg('page_has_one_alert_singular'); 703 } else { 704 utterance += msg('page_has_alerts_plural', 705 [alerts.length]); 706 } 707 708 for (var i = 0; i < alerts.length; i++) { 709 utterance += ' ' + alerts[i].message; 710 } 711 712 utterance += ' ' + msg('review_alerts'); 713 714 if (this.idleSpeechQueue_.indexOf(utterance) == -1) { 715 this.idleSpeechQueue_.push(utterance); 716 } 717 }, this)); 718 }, this)); 719}; 720