| OLD | NEW |
| (Empty) | |
| 1 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 /** |
| 6 * @fileoverview ChromeVox commands. |
| 7 */ |
| 8 |
| 9 goog.provide('CommandHandler'); |
| 10 |
| 11 goog.require('ChromeVoxState'); |
| 12 goog.require('Output'); |
| 13 goog.require('cvox.ChromeVoxBackground'); |
| 14 |
| 15 goog.scope(function() { |
| 16 var AutomationNode = chrome.automation.AutomationNode; |
| 17 var Dir = constants.Dir; |
| 18 var EventType = chrome.automation.EventType; |
| 19 var RoleType = chrome.automation.RoleType; |
| 20 |
| 21 /** |
| 22 * Handles ChromeVox Next commands. |
| 23 * @param {string} command |
| 24 * @return {boolean} True if the command should propagate. |
| 25 */ |
| 26 CommandHandler.onCommand = function(command) { |
| 27 // Check for loss of focus which results in us invalidating our current |
| 28 // range. Note this call is synchronis. |
| 29 chrome.automation.getFocus(function(focusedNode) { |
| 30 var cur = ChromeVoxState.instance.currentRange; |
| 31 if (cur && !cur.isValid()) { |
| 32 ChromeVoxState.instance.setCurrentRange( |
| 33 cursors.Range.fromNode(focusedNode)); |
| 34 } |
| 35 |
| 36 if (!focusedNode) |
| 37 ChromeVoxState.instance.setCurrentRange(null); |
| 38 }); |
| 39 |
| 40 // These commands don't require a current range and work in all modes. |
| 41 switch (command) { |
| 42 case 'speakTimeAndDate': |
| 43 chrome.automation.getDesktop(function(d) { |
| 44 // First, try speaking the on-screen time. |
| 45 var allTime = d.findAll({role: RoleType.time}); |
| 46 allTime.filter(function(t) { return t.root.role == RoleType.desktop; }); |
| 47 |
| 48 var timeString = ''; |
| 49 allTime.forEach(function(t) { |
| 50 if (t.name) timeString = t.name; |
| 51 }); |
| 52 if (timeString) { |
| 53 cvox.ChromeVox.tts.speak(timeString, cvox.QueueMode.FLUSH); |
| 54 } else { |
| 55 // Fallback to the old way of speaking time. |
| 56 var output = new Output(); |
| 57 var dateTime = new Date(); |
| 58 output |
| 59 .withString( |
| 60 dateTime.toLocaleTimeString() + ', ' + |
| 61 dateTime.toLocaleDateString()) |
| 62 .go(); |
| 63 } |
| 64 }); |
| 65 return false; |
| 66 case 'showOptionsPage': |
| 67 chrome.runtime.openOptionsPage(); |
| 68 break; |
| 69 case 'toggleChromeVox': |
| 70 if (cvox.ChromeVox.isChromeOS) |
| 71 return false; |
| 72 |
| 73 cvox.ChromeVox.isActive = !cvox.ChromeVox.isActive; |
| 74 if (!cvox.ChromeVox.isActive) { |
| 75 var msg = Msgs.getMsg('chromevox_inactive'); |
| 76 cvox.ChromeVox.tts.speak(msg, cvox.QueueMode.FLUSH); |
| 77 return false; |
| 78 } |
| 79 break; |
| 80 case 'toggleStickyMode': |
| 81 cvox.ChromeVoxBackground.setPref( |
| 82 'sticky', !cvox.ChromeVox.isStickyPrefOn, true); |
| 83 |
| 84 if (cvox.ChromeVox.isStickyPrefOn) |
| 85 chrome.accessibilityPrivate.setKeyboardListener(true, true); |
| 86 else |
| 87 chrome.accessibilityPrivate.setKeyboardListener(true, false); |
| 88 return false; |
| 89 case 'passThroughMode': |
| 90 cvox.ChromeVox.passThroughMode = true; |
| 91 cvox.ChromeVox.tts.speak( |
| 92 Msgs.getMsg('pass_through_key'), cvox.QueueMode.QUEUE); |
| 93 return true; |
| 94 case 'showKbExplorerPage': |
| 95 var explorerPage = {url: 'chromevox/background/kbexplorer.html'}; |
| 96 chrome.tabs.create(explorerPage); |
| 97 break; |
| 98 case 'decreaseTtsRate': |
| 99 CommandHandler.increaseOrDecreaseSpeechProperty_( |
| 100 cvox.AbstractTts.RATE, false); |
| 101 return false; |
| 102 case 'increaseTtsRate': |
| 103 CommandHandler.increaseOrDecreaseSpeechProperty_( |
| 104 cvox.AbstractTts.RATE, true); |
| 105 return false; |
| 106 case 'decreaseTtsPitch': |
| 107 CommandHandler.increaseOrDecreaseSpeechProperty_( |
| 108 cvox.AbstractTts.PITCH, false); |
| 109 return false; |
| 110 case 'increaseTtsPitch': |
| 111 CommandHandler.increaseOrDecreaseSpeechProperty_( |
| 112 cvox.AbstractTts.PITCH, true); |
| 113 return false; |
| 114 case 'decreaseTtsVolume': |
| 115 CommandHandler.increaseOrDecreaseSpeechProperty_( |
| 116 cvox.AbstractTts.VOLUME, false); |
| 117 return false; |
| 118 case 'increaseTtsVolume': |
| 119 CommandHandler.increaseOrDecreaseSpeechProperty_( |
| 120 cvox.AbstractTts.VOLUME, true); |
| 121 return false; |
| 122 case 'stopSpeech': |
| 123 cvox.ChromeVox.tts.stop(); |
| 124 ChromeVoxState.isReadingContinuously = false; |
| 125 return false; |
| 126 case 'toggleEarcons': |
| 127 cvox.AbstractEarcons.enabled = !cvox.AbstractEarcons.enabled; |
| 128 var announce = cvox.AbstractEarcons.enabled ? Msgs.getMsg('earcons_on') : |
| 129 Msgs.getMsg('earcons_off'); |
| 130 cvox.ChromeVox.tts.speak( |
| 131 announce, cvox.QueueMode.FLUSH, |
| 132 cvox.AbstractTts.PERSONALITY_ANNOTATION); |
| 133 return false; |
| 134 case 'cycleTypingEcho': |
| 135 cvox.ChromeVox.typingEcho = |
| 136 cvox.TypingEcho.cycle(cvox.ChromeVox.typingEcho); |
| 137 var announce = ''; |
| 138 switch (cvox.ChromeVox.typingEcho) { |
| 139 case cvox.TypingEcho.CHARACTER: |
| 140 announce = Msgs.getMsg('character_echo'); |
| 141 break; |
| 142 case cvox.TypingEcho.WORD: |
| 143 announce = Msgs.getMsg('word_echo'); |
| 144 break; |
| 145 case cvox.TypingEcho.CHARACTER_AND_WORD: |
| 146 announce = Msgs.getMsg('character_and_word_echo'); |
| 147 break; |
| 148 case cvox.TypingEcho.NONE: |
| 149 announce = Msgs.getMsg('none_echo'); |
| 150 break; |
| 151 } |
| 152 cvox.ChromeVox.tts.speak( |
| 153 announce, cvox.QueueMode.FLUSH, |
| 154 cvox.AbstractTts.PERSONALITY_ANNOTATION); |
| 155 return false; |
| 156 case 'cyclePunctuationEcho': |
| 157 cvox.ChromeVox.tts.speak( |
| 158 Msgs.getMsg(ChromeVoxState.backgroundTts.cyclePunctuationEcho()), |
| 159 cvox.QueueMode.FLUSH); |
| 160 return false; |
| 161 case 'reportIssue': |
| 162 var url = Background.ISSUE_URL; |
| 163 var description = {}; |
| 164 description['Mode'] = ChromeVoxState.instance.mode; |
| 165 description['Version'] = chrome.app.getDetails().version; |
| 166 description['Reproduction Steps'] = '%0a1.%0a2.%0a3.'; |
| 167 for (var key in description) |
| 168 url += key + ':%20' + description[key] + '%0a'; |
| 169 chrome.tabs.create({url: url}); |
| 170 return false; |
| 171 case 'toggleBrailleCaptions': |
| 172 cvox.BrailleCaptionsBackground.setActive( |
| 173 !cvox.BrailleCaptionsBackground.isEnabled()); |
| 174 return false; |
| 175 case 'toggleChromeVoxVersion': |
| 176 if (!ChromeVoxState.instance.toggleNext()) |
| 177 return false; |
| 178 if (ChromeVoxState.instance.currentRange) { |
| 179 ChromeVoxState.instance.navigateToRange( |
| 180 ChromeVoxState.instance.currentRange); |
| 181 } |
| 182 break; |
| 183 case 'showNextUpdatePage': |
| 184 (new PanelCommand(PanelCommandType.TUTORIAL)).send(); |
| 185 return false; |
| 186 default: |
| 187 break; |
| 188 } |
| 189 |
| 190 // Require a current range. |
| 191 if (!ChromeVoxState.instance.currentRange_) |
| 192 return true; |
| 193 |
| 194 // Next/compat commands hereafter. |
| 195 if (ChromeVoxState.instance.mode == ChromeVoxMode.CLASSIC) return true; |
| 196 |
| 197 var current = ChromeVoxState.instance.currentRange_; |
| 198 var dir = Dir.FORWARD; |
| 199 var pred = null; |
| 200 var predErrorMsg = undefined; |
| 201 var speechProps = {}; |
| 202 switch (command) { |
| 203 case 'nextCharacter': |
| 204 speechProps['phoneticCharacters'] = true; |
| 205 current = current.move(cursors.Unit.CHARACTER, Dir.FORWARD); |
| 206 break; |
| 207 case 'previousCharacter': |
| 208 speechProps['phoneticCharacters'] = true; |
| 209 current = current.move(cursors.Unit.CHARACTER, Dir.BACKWARD); |
| 210 break; |
| 211 case 'nextWord': |
| 212 current = current.move(cursors.Unit.WORD, Dir.FORWARD); |
| 213 break; |
| 214 case 'previousWord': |
| 215 current = current.move(cursors.Unit.WORD, Dir.BACKWARD); |
| 216 break; |
| 217 case 'forward': |
| 218 case 'nextLine': |
| 219 current = current.move(cursors.Unit.LINE, Dir.FORWARD); |
| 220 break; |
| 221 case 'backward': |
| 222 case 'previousLine': |
| 223 current = current.move(cursors.Unit.LINE, Dir.BACKWARD); |
| 224 break; |
| 225 case 'nextButton': |
| 226 dir = Dir.FORWARD; |
| 227 pred = AutomationPredicate.button; |
| 228 predErrorMsg = 'no_next_button'; |
| 229 break; |
| 230 case 'previousButton': |
| 231 dir = Dir.BACKWARD; |
| 232 pred = AutomationPredicate.button; |
| 233 predErrorMsg = 'no_previous_button'; |
| 234 break; |
| 235 case 'nextCheckbox': |
| 236 dir = Dir.FORWARD; |
| 237 pred = AutomationPredicate.checkBox; |
| 238 predErrorMsg = 'no_next_checkbox'; |
| 239 break; |
| 240 case 'previousCheckbox': |
| 241 dir = Dir.BACKWARD; |
| 242 pred = AutomationPredicate.checkBox; |
| 243 predErrorMsg = 'no_previous_checkbox'; |
| 244 break; |
| 245 case 'nextComboBox': |
| 246 dir = Dir.FORWARD; |
| 247 pred = AutomationPredicate.comboBox; |
| 248 predErrorMsg = 'no_next_combo_box'; |
| 249 break; |
| 250 case 'previousComboBox': |
| 251 dir = Dir.BACKWARD; |
| 252 pred = AutomationPredicate.comboBox; |
| 253 predErrorMsg = 'no_previous_combo_box'; |
| 254 break; |
| 255 case 'nextEditText': |
| 256 dir = Dir.FORWARD; |
| 257 pred = AutomationPredicate.editText; |
| 258 predErrorMsg = 'no_next_edit_text'; |
| 259 break; |
| 260 case 'previousEditText': |
| 261 dir = Dir.BACKWARD; |
| 262 pred = AutomationPredicate.editText; |
| 263 predErrorMsg = 'no_previous_edit_text'; |
| 264 break; |
| 265 case 'nextFormField': |
| 266 dir = Dir.FORWARD; |
| 267 pred = AutomationPredicate.formField; |
| 268 predErrorMsg = 'no_next_form_field'; |
| 269 break; |
| 270 case 'previousFormField': |
| 271 dir = Dir.BACKWARD; |
| 272 pred = AutomationPredicate.formField; |
| 273 predErrorMsg = 'no_previous_form_field'; |
| 274 break; |
| 275 case 'nextHeading': |
| 276 dir = Dir.FORWARD; |
| 277 pred = AutomationPredicate.heading; |
| 278 predErrorMsg = 'no_next_heading'; |
| 279 break; |
| 280 case 'previousHeading': |
| 281 dir = Dir.BACKWARD; |
| 282 pred = AutomationPredicate.heading; |
| 283 predErrorMsg = 'no_previous_heading'; |
| 284 break; |
| 285 case 'nextLink': |
| 286 dir = Dir.FORWARD; |
| 287 pred = AutomationPredicate.link; |
| 288 predErrorMsg = 'no_next_link'; |
| 289 break; |
| 290 case 'previousLink': |
| 291 dir = Dir.BACKWARD; |
| 292 pred = AutomationPredicate.link; |
| 293 predErrorMsg = 'no_previous_link'; |
| 294 break; |
| 295 case 'nextTable': |
| 296 dir = Dir.FORWARD; |
| 297 pred = AutomationPredicate.table; |
| 298 predErrorMsg = 'no_next_table'; |
| 299 break; |
| 300 case 'previousTable': |
| 301 dir = Dir.BACKWARD; |
| 302 pred = AutomationPredicate.table; |
| 303 predErrorMsg = 'no_previous_table'; |
| 304 break; |
| 305 case 'nextVisitedLink': |
| 306 dir = Dir.FORWARD; |
| 307 pred = AutomationPredicate.visitedLink; |
| 308 predErrorMsg = 'no_next_visited_link'; |
| 309 break; |
| 310 case 'previousVisitedLink': |
| 311 dir = Dir.BACKWARD; |
| 312 pred = AutomationPredicate.visitedLink; |
| 313 predErrorMsg = 'no_previous_visited_link'; |
| 314 break; |
| 315 case 'right': |
| 316 case 'nextObject': |
| 317 current = current.move(cursors.Unit.DOM_NODE, Dir.FORWARD); |
| 318 break; |
| 319 case 'left': |
| 320 case 'previousObject': |
| 321 current = current.move(cursors.Unit.DOM_NODE, Dir.BACKWARD); |
| 322 break; |
| 323 case 'jumpToTop': |
| 324 var node = AutomationUtil.findNodePost( |
| 325 current.start.node.root, Dir.FORWARD, AutomationPredicate.leaf); |
| 326 if (node) |
| 327 current = cursors.Range.fromNode(node); |
| 328 break; |
| 329 case 'jumpToBottom': |
| 330 var node = AutomationUtil.findNodePost( |
| 331 current.start.node.root, Dir.BACKWARD, AutomationPredicate.leaf); |
| 332 if (node) |
| 333 current = cursors.Range.fromNode(node); |
| 334 break; |
| 335 case 'forceClickOnCurrentItem': |
| 336 if (ChromeVoxState.instance.currentRange_) { |
| 337 var actionNode = ChromeVoxState.instance.currentRange_.start.node; |
| 338 if (actionNode.role == RoleType.inlineTextBox) |
| 339 actionNode = actionNode.parent; |
| 340 actionNode.doDefault(); |
| 341 } |
| 342 // Skip all other processing; if focus changes, we should get an event |
| 343 // for that. |
| 344 return false; |
| 345 case 'readFromHere': |
| 346 ChromeVoxState.isReadingContinuously = true; |
| 347 var continueReading = function() { |
| 348 if (!ChromeVoxState.isReadingContinuously || |
| 349 !ChromeVoxState.instance.currentRange_) |
| 350 return; |
| 351 |
| 352 var prevRange = ChromeVoxState.instance.currentRange_; |
| 353 var newRange = |
| 354 ChromeVoxState.instance.currentRange_.move( |
| 355 cursors.Unit.DOM_NODE, Dir.FORWARD); |
| 356 |
| 357 // Stop if we've wrapped back to the document. |
| 358 var maybeDoc = newRange.start.node; |
| 359 if (maybeDoc.role == RoleType.rootWebArea && |
| 360 maybeDoc.parent.root.role == RoleType.desktop) { |
| 361 ChromeVoxState.isReadingContinuously = false; |
| 362 return; |
| 363 } |
| 364 |
| 365 ChromeVoxState.instance.setCurrentRange(newRange); |
| 366 |
| 367 new Output() |
| 368 .withRichSpeechAndBraille(ChromeVoxState.instance.currentRange_, |
| 369 prevRange, |
| 370 Output.EventType.NAVIGATE) |
| 371 .onSpeechEnd(continueReading) |
| 372 .go(); |
| 373 }.bind(this); |
| 374 |
| 375 new Output() |
| 376 .withRichSpeechAndBraille(ChromeVoxState.instance.currentRange_, |
| 377 null, |
| 378 Output.EventType.NAVIGATE) |
| 379 .onSpeechEnd(continueReading) |
| 380 .go(); |
| 381 |
| 382 return false; |
| 383 case 'contextMenu': |
| 384 if (ChromeVoxState.instance.currentRange_) { |
| 385 var actionNode = ChromeVoxState.instance.currentRange_.start.node; |
| 386 if (actionNode.role == RoleType.inlineTextBox) |
| 387 actionNode = actionNode.parent; |
| 388 actionNode.showContextMenu(); |
| 389 return false; |
| 390 } |
| 391 break; |
| 392 case 'toggleKeyboardHelp': |
| 393 ChromeVoxState.instance.startExcursion(); |
| 394 (new PanelCommand(PanelCommandType.OPEN_MENUS)).send(); |
| 395 return false; |
| 396 case 'showHeadingsList': |
| 397 ChromeVoxState.instance.startExcursion(); |
| 398 (new PanelCommand(PanelCommandType.OPEN_MENUS, 'role_heading')).send(); |
| 399 return false; |
| 400 case 'showFormsList': |
| 401 ChromeVoxState.instance.startExcursion(); |
| 402 (new PanelCommand(PanelCommandType.OPEN_MENUS, 'role_form')).send(); |
| 403 return false; |
| 404 case 'showLandmarksList': |
| 405 ChromeVoxState.instance.startExcursion(); |
| 406 (new PanelCommand(PanelCommandType.OPEN_MENUS, 'role_landmark')).send(); |
| 407 return false; |
| 408 case 'showLinksList': |
| 409 ChromeVoxState.instance.startExcursion(); |
| 410 (new PanelCommand(PanelCommandType.OPEN_MENUS, 'role_link')).send(); |
| 411 return false; |
| 412 case 'showTablesList': |
| 413 ChromeVoxState.instance.startExcursion(); |
| 414 (new PanelCommand(PanelCommandType.OPEN_MENUS, 'table_strategy')).send(); |
| 415 return false; |
| 416 case 'toggleSearchWidget': |
| 417 (new PanelCommand(PanelCommandType.SEARCH)).send(); |
| 418 return false; |
| 419 case 'readCurrentTitle': |
| 420 var target = ChromeVoxState.instance.currentRange_.start.node; |
| 421 var output = new Output(); |
| 422 |
| 423 if (target.root.role == RoleType.rootWebArea) { |
| 424 // Web. |
| 425 target = target.root; |
| 426 output.withString(target.name || target.docUrl); |
| 427 } else { |
| 428 // Views. |
| 429 while (target.role != RoleType.window) target = target.parent; |
| 430 if (target) |
| 431 output.withString(target.name || ''); |
| 432 } |
| 433 output.go(); |
| 434 return false; |
| 435 case 'readCurrentURL': |
| 436 var output = new Output(); |
| 437 var target = ChromeVoxState.instance.currentRange_.start.node.root; |
| 438 output.withString(target.docUrl || '').go(); |
| 439 return false; |
| 440 case 'copy': |
| 441 var textarea = document.createElement('textarea'); |
| 442 document.body.appendChild(textarea); |
| 443 textarea.focus(); |
| 444 document.execCommand('paste'); |
| 445 var clipboardContent = textarea.value; |
| 446 textarea.remove(); |
| 447 cvox.ChromeVox.tts.speak( |
| 448 Msgs.getMsg('copy', [clipboardContent]), cvox.QueueMode.FLUSH); |
| 449 ChromeVoxState.instance.pageSel_ = null; |
| 450 return true; |
| 451 case 'toggleSelection': |
| 452 if (!ChromeVoxState.instance.pageSel_) { |
| 453 ChromeVoxState.instance.pageSel_ = ChromeVoxState.instance.currentRange; |
| 454 } else { |
| 455 var root = ChromeVoxState.instance.currentRange_.start.node.root; |
| 456 if (root && root.anchorObject && root.focusObject) { |
| 457 var sel = new cursors.Range( |
| 458 new cursors.Cursor(root.anchorObject, root.anchorOffset), |
| 459 new cursors.Cursor(root.focusObject, root.focusOffset)); |
| 460 var o = new Output() |
| 461 .format('@end_selection') |
| 462 .withSpeechAndBraille(sel, sel, Output.EventType.NAVIGATE) |
| 463 .go(); |
| 464 } |
| 465 ChromeVoxState.instance.pageSel_ = null; |
| 466 return false; |
| 467 } |
| 468 break; |
| 469 default: |
| 470 return true; |
| 471 } |
| 472 |
| 473 if (pred) { |
| 474 var bound = current.getBound(dir).node; |
| 475 if (bound) { |
| 476 var node = AutomationUtil.findNextNode( |
| 477 bound, dir, pred, {skipInitialAncestry: true}); |
| 478 |
| 479 if (node) { |
| 480 node = AutomationUtil.findNodePre( |
| 481 node, Dir.FORWARD, AutomationPredicate.object) || |
| 482 node; |
| 483 } |
| 484 |
| 485 if (node) { |
| 486 current = cursors.Range.fromNode(node); |
| 487 } else { |
| 488 if (predErrorMsg) { |
| 489 cvox.ChromeVox.tts.speak( |
| 490 Msgs.getMsg(predErrorMsg), cvox.QueueMode.FLUSH); |
| 491 } |
| 492 return false; |
| 493 } |
| 494 } |
| 495 } |
| 496 |
| 497 if (current) |
| 498 ChromeVoxState.instance.navigateToRange(current, undefined, speechProps); |
| 499 |
| 500 return false; |
| 501 }; |
| 502 |
| 503 /** |
| 504 * React to mode changes. |
| 505 * @param {ChromeVoxMode} newMode |
| 506 * @param {ChromeVoxMode?} oldMode |
| 507 */ |
| 508 CommandHandler.onModeChanged = function(newMode, oldMode) { |
| 509 // Previously uninitialized. |
| 510 if (!oldMode) |
| 511 cvox.ChromeVoxKbHandler.commandHandler = CommandHandler.onCommand; |
| 512 |
| 513 var hasListener = |
| 514 chrome.commands.onCommand.hasListener(CommandHandler.onCommand); |
| 515 if (newMode == ChromeVoxMode.CLASSIC && hasListener) |
| 516 chrome.commands.onCommand.removeListener(CommandHandler.onCommand); |
| 517 else if (newMode == ChromeVoxMode.CLASSIC && !hasListener) |
| 518 chrome.commands.onCommand.addListener(CommandHandler.onCommand); |
| 519 }; |
| 520 |
| 521 /** |
| 522 * Increase or decrease a speech property and make an announcement. |
| 523 * @param {string} propertyName The name of the property to change. |
| 524 * @param {boolean} increase If true, increases the property value by one |
| 525 * step size, otherwise decreases. |
| 526 * @private |
| 527 */ |
| 528 CommandHandler.increaseOrDecreaseSpeechProperty_ = |
| 529 function(propertyName, increase) { |
| 530 cvox.ChromeVox.tts.increaseOrDecreaseProperty(propertyName, increase); |
| 531 var announcement; |
| 532 var valueAsPercent = Math.round( |
| 533 cvox.ChromeVox.tts.propertyToPercentage(propertyName) * 100); |
| 534 switch (propertyName) { |
| 535 case cvox.AbstractTts.RATE: |
| 536 announcement = Msgs.getMsg('announce_rate', [valueAsPercent]); |
| 537 break; |
| 538 case cvox.AbstractTts.PITCH: |
| 539 announcement = Msgs.getMsg('announce_pitch', [valueAsPercent]); |
| 540 break; |
| 541 case cvox.AbstractTts.VOLUME: |
| 542 announcement = Msgs.getMsg('announce_volume', [valueAsPercent]); |
| 543 break; |
| 544 } |
| 545 if (announcement) { |
| 546 cvox.ChromeVox.tts.speak( |
| 547 announcement, cvox.QueueMode.FLUSH, |
| 548 cvox.AbstractTts.PERSONALITY_ANNOTATION); |
| 549 } |
| 550 }; |
| 551 |
| 552 }); // goog.scope |
| OLD | NEW |