chromevox.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979
  1. define(function(require, exports, module) {
  2. /* ChromeVox Ace namespace. */
  3. var cvoxAce = {};
  4. /* Typedefs for Closure compiler. */
  5. /**
  6. * @typedef {{
  7. rate: number,
  8. pitch: number,
  9. volume: number,
  10. relativePitch: number,
  11. punctuationEcho: string
  12. }}
  13. */
  14. /* TODO(peterxiao): Export this typedef through cvox.Api. */
  15. cvoxAce.SpeechProperty;
  16. /**
  17. * @typedef {{
  18. * row: number,
  19. * column: number
  20. * }}
  21. */
  22. cvoxAce.Cursor;
  23. /**
  24. * @typedef {{
  25. type: string,
  26. value: string
  27. }}
  28. }
  29. */
  30. cvoxAce.Token;
  31. /**
  32. * These are errors and information that Ace will display in the gutter.
  33. * @typedef {{
  34. row: number,
  35. column: number,
  36. value: string
  37. }}
  38. }
  39. */
  40. cvoxAce.Annotation;
  41. /* Speech Properties. */
  42. /**
  43. * Speech property for speaking constant tokens.
  44. * @type {cvoxAce.SpeechProperty}
  45. */
  46. var CONSTANT_PROP = {
  47. 'rate': 0.8,
  48. 'pitch': 0.4,
  49. 'volume': 0.9
  50. };
  51. /**
  52. * Default speech property for speaking tokens.
  53. * @type {cvoxAce.SpeechProperty}
  54. */
  55. var DEFAULT_PROP = {
  56. 'rate': 1,
  57. 'pitch': 0.5,
  58. 'volume': 0.9
  59. };
  60. /**
  61. * Speech property for speaking entity tokens.
  62. * @type {cvoxAce.SpeechProperty}
  63. */
  64. var ENTITY_PROP = {
  65. 'rate': 0.8,
  66. 'pitch': 0.8,
  67. 'volume': 0.9
  68. };
  69. /**
  70. * Speech property for speaking keywords.
  71. * @type {cvoxAce.SpeechProperty}
  72. */
  73. var KEYWORD_PROP = {
  74. 'rate': 0.8,
  75. 'pitch': 0.3,
  76. 'volume': 0.9
  77. };
  78. /**
  79. * Speech property for speaking storage tokens.
  80. * @type {cvoxAce.SpeechProperty}
  81. */
  82. var STORAGE_PROP = {
  83. 'rate': 0.8,
  84. 'pitch': 0.7,
  85. 'volume': 0.9
  86. };
  87. /**
  88. * Speech property for speaking variable tokens.
  89. * @type {cvoxAce.SpeechProperty}
  90. */
  91. var VARIABLE_PROP = {
  92. 'rate': 0.8,
  93. 'pitch': 0.8,
  94. 'volume': 0.9
  95. };
  96. /**
  97. * Speech property for speaking deleted text.
  98. * @type {cvoxAce.SpeechProperty}
  99. */
  100. var DELETED_PROP = {
  101. 'punctuationEcho': 'none',
  102. 'relativePitch': -0.6
  103. };
  104. /* Constants for Earcons. */
  105. var ERROR_EARCON = 'ALERT_NONMODAL';
  106. var MODE_SWITCH_EARCON = 'ALERT_MODAL';
  107. var NO_MATCH_EARCON = 'INVALID_KEYPRESS';
  108. /* Constants for vim state. */
  109. var INSERT_MODE_STATE = 'insertMode';
  110. var COMMAND_MODE_STATE = 'start';
  111. var REPLACE_LIST = [
  112. {
  113. substr: ';',
  114. newSubstr: ' semicolon '
  115. },
  116. {
  117. substr: ':',
  118. newSubstr: ' colon '
  119. }
  120. ];
  121. /**
  122. * Context menu commands.
  123. */
  124. var Command = {
  125. SPEAK_ANNOT: 'annots',
  126. SPEAK_ALL_ANNOTS: 'all_annots',
  127. TOGGLE_LOCATION: 'toggle_location',
  128. SPEAK_MODE: 'mode',
  129. SPEAK_ROW_COL: 'row_col',
  130. TOGGLE_DISPLACEMENT: 'toggle_displacement',
  131. FOCUS_TEXT: 'focus_text'
  132. };
  133. /**
  134. * Key prefix for each shortcut.
  135. */
  136. var KEY_PREFIX = 'CONTROL + SHIFT ';
  137. /* Globals. */
  138. cvoxAce.editor = null;
  139. /**
  140. * Last cursor position.
  141. * @type {cvoxAce.Cursor}
  142. */
  143. var lastCursor = null;
  144. /**
  145. * Table of annotations.
  146. * @typedef {!Object.<number, Object<number, cvoxAce.Annotation>>}
  147. */
  148. var annotTable = {};
  149. /**
  150. * Whether to speak character, word, and then line. This allows blind users
  151. * to know the location of the cursor when they change lines.
  152. * @typedef {boolean}
  153. */
  154. var shouldSpeakRowLocation = false;
  155. /**
  156. * Whether to speak displacement.
  157. * @typedef {boolean}
  158. */
  159. var shouldSpeakDisplacement = false;
  160. /**
  161. * Whether text was changed to cause a cursor change event.
  162. * @typedef {boolean}
  163. */
  164. var changed = false;
  165. /**
  166. * Current state vim is in.
  167. */
  168. var vimState = null;
  169. /**
  170. * Mapping from key code to shortcut.
  171. */
  172. var keyCodeToShortcutMap = {};
  173. /**
  174. * Mapping from command to shortcut.
  175. */
  176. var cmdToShortcutMap = {};
  177. /**
  178. * Get shortcut string from keyCode.
  179. * @param {number} keyCode Key code of shortcut.
  180. * @return {string} String representation of shortcut.
  181. */
  182. var getKeyShortcutString = function(keyCode) {
  183. return KEY_PREFIX + String.fromCharCode(keyCode);
  184. };
  185. /**
  186. * Return if in vim mode.
  187. * @return {boolean} True if in Vim mode.
  188. */
  189. var isVimMode = function() {
  190. var keyboardHandler = cvoxAce.editor.keyBinding.getKeyboardHandler();
  191. return keyboardHandler.$id === 'ace/keyboard/vim';
  192. };
  193. /**
  194. * Gets the current token.
  195. * @param {!cvoxAce.Cursor} cursor Current position of the cursor.
  196. * @return {!cvoxAce.Token} Token at the current position.
  197. */
  198. var getCurrentToken = function(cursor) {
  199. return cvoxAce.editor.getSession().getTokenAt(cursor.row, cursor.column + 1);
  200. };
  201. /**
  202. * Gets the current line the cursor is under.
  203. * @param {!cvoxAce.Cursor} cursor Current cursor position.
  204. */
  205. var getCurrentLine = function(cursor) {
  206. return cvoxAce.editor.getSession().getLine(cursor.row);
  207. };
  208. /**
  209. * Event handler for row changes. When the user changes rows we want to speak
  210. * the line so the user can work on this line. If shouldSpeakRowLocation is on
  211. * then we speak the character, then the row, then the line so the user knows
  212. * where the cursor is.
  213. * @param {!cvoxAce.Cursor} currCursor Current cursor position.
  214. */
  215. var onRowChange = function(currCursor) {
  216. /* Notify that this line has an annotation. */
  217. if (annotTable[currCursor.row]) {
  218. cvox.Api.playEarcon(ERROR_EARCON);
  219. }
  220. if (shouldSpeakRowLocation) {
  221. cvox.Api.stop();
  222. speakChar(currCursor);
  223. speakTokenQueue(getCurrentToken(currCursor));
  224. speakLine(currCursor.row, 1);
  225. } else {
  226. speakLine(currCursor.row, 0);
  227. }
  228. };
  229. /**
  230. * Returns whether the cursor is at the beginning of a word. A word is
  231. * a grouping of alphanumeric characters including underscores.
  232. * @param {!cvoxAce.Cursor} cursor Current cursor position.
  233. * @return {boolean} Whether there is word.
  234. */
  235. var isWord = function(cursor) {
  236. var line = getCurrentLine(cursor);
  237. var lineSuffix = line.substr(cursor.column - 1);
  238. if (cursor.column === 0) {
  239. lineSuffix = ' ' + line;
  240. }
  241. /* Use regex to tell if the suffix is at the start of a new word. */
  242. var firstWordRegExp = /^\W(\w+)/;
  243. var words = firstWordRegExp.exec(lineSuffix);
  244. return words !== null;
  245. };
  246. /**
  247. * A mapping of syntax type to speech properties / expanding rules.
  248. */
  249. var rules = {
  250. 'constant': {
  251. prop: CONSTANT_PROP
  252. },
  253. 'entity': {
  254. prop: ENTITY_PROP
  255. },
  256. 'keyword': {
  257. prop: KEYWORD_PROP
  258. },
  259. 'storage': {
  260. prop: STORAGE_PROP
  261. },
  262. 'variable': {
  263. prop: VARIABLE_PROP
  264. },
  265. 'meta': {
  266. prop: DEFAULT_PROP,
  267. replace: [
  268. {
  269. substr: '</',
  270. newSubstr: ' closing tag '
  271. },
  272. {
  273. substr: '/>',
  274. newSubstr: ' close tag '
  275. },
  276. {
  277. substr: '<',
  278. newSubstr: ' tag start '
  279. },
  280. {
  281. substr: '>',
  282. newSubstr: ' tag end '
  283. }
  284. ]
  285. }
  286. };
  287. /**
  288. * Default rule to be used.
  289. */
  290. var DEFAULT_RULE = {
  291. prop: DEFAULT_RULE
  292. };
  293. /**
  294. * Expands substrings to how they are read based on the given rules.
  295. * @param {string} value Text to be expanded.
  296. * @param {Array.<Object>} replaceRules Rules to determine expansion.
  297. * @return {string} New expanded value.
  298. */
  299. var expand = function(value, replaceRules) {
  300. var newValue = value;
  301. for (var i = 0; i < replaceRules.length; i++) {
  302. var replaceRule = replaceRules[i];
  303. var regexp = new RegExp(replaceRule.substr, 'g');
  304. newValue = newValue.replace(regexp, replaceRule.newSubstr);
  305. }
  306. return newValue;
  307. };
  308. /**
  309. * Merges tokens from start inclusive to end exclusive.
  310. * @param {Array.<cvoxAce.Token>} Tokens to be merged.
  311. * @param {number} start Start index inclusive.
  312. * @param {number} end End index exclusive.
  313. * @return {cvoxAce.Token} Merged token.
  314. */
  315. var mergeTokens = function(tokens, start, end) {
  316. /* Different type of token found! Merge all previous like tokens. */
  317. var newToken = {};
  318. newToken.value = '';
  319. newToken.type = tokens[start].type;
  320. for (var j = start; j < end; j++) {
  321. newToken.value += tokens[j].value;
  322. }
  323. return newToken;
  324. };
  325. /**
  326. * Merges tokens that use the same speech properties.
  327. * @param {Array.<cvoxAce.Token>} tokens Tokens to be merged.
  328. * @return {Array.<cvoxAce.Token>} Merged tokens.
  329. */
  330. var mergeLikeTokens = function(tokens) {
  331. if (tokens.length <= 1) {
  332. return tokens;
  333. }
  334. var newTokens = [];
  335. var lastLikeIndex = 0;
  336. for (var i = 1; i < tokens.length; i++) {
  337. var lastLikeToken = tokens[lastLikeIndex];
  338. var currToken = tokens[i];
  339. if (getTokenRule(lastLikeToken) !== getTokenRule(currToken)) {
  340. newTokens.push(mergeTokens(tokens, lastLikeIndex, i));
  341. lastLikeIndex = i;
  342. }
  343. }
  344. newTokens.push(mergeTokens(tokens, lastLikeIndex, tokens.length));
  345. return newTokens;
  346. };
  347. /**
  348. * Returns if given row is a whitespace row.
  349. * @param {number} row Row.
  350. * @return {boolean} True if row is whitespaces.
  351. */
  352. var isRowWhiteSpace = function(row) {
  353. var line = cvoxAce.editor.getSession().getLine(row);
  354. var whiteSpaceRegexp = /^\s*$/;
  355. return whiteSpaceRegexp.exec(line) !== null;
  356. };
  357. /**
  358. * Speak the line with syntax properties.
  359. * @param {number} row Row to speak.
  360. * @param {number} queue Queue mode to speak.
  361. */
  362. var speakLine = function(row, queue) {
  363. var tokens = cvoxAce.editor.getSession().getTokens(row);
  364. if (tokens.length === 0 || isRowWhiteSpace(row)) {
  365. cvox.Api.playEarcon('EDITABLE_TEXT');
  366. return;
  367. }
  368. tokens = mergeLikeTokens(tokens);
  369. var firstToken = tokens[0];
  370. /* Filter out first token. */
  371. tokens = tokens.filter(function(token) {
  372. return token !== firstToken;
  373. });
  374. /* Speak first token separately to flush if queue. */
  375. speakToken_(firstToken, queue);
  376. /* Speak rest of tokens. */
  377. tokens.forEach(speakTokenQueue);
  378. };
  379. /**
  380. * Speak the token based on the syntax of the token, flushing.
  381. * @param {!cvoxAce.Token} token Token to speak.
  382. * @param {number} queue Queue mode.
  383. */
  384. var speakTokenFlush = function(token) {
  385. speakToken_(token, 0);
  386. };
  387. /**
  388. * Speak the token based on the syntax of the token, queueing.
  389. * @param {!cvoxAce.Token} token Token to speak.
  390. * @param {number} queue Queue mode.
  391. */
  392. var speakTokenQueue = function(token) {
  393. speakToken_(token, 1);
  394. };
  395. /**
  396. * @param {!cvoxAce.Token} token Token to speak.
  397. * Get the token speech property.
  398. */
  399. var getTokenRule = function(token) {
  400. /* Types are period delimited. In this case, we only syntax speak the outer
  401. * most type of token. */
  402. if (!token || !token.type) {
  403. return;
  404. }
  405. var split = token.type.split('.');
  406. if (split.length === 0) {
  407. return;
  408. }
  409. var type = split[0];
  410. var rule = rules[type];
  411. if (!rule) {
  412. return DEFAULT_RULE;
  413. }
  414. return rule;
  415. };
  416. /**
  417. * Speak the token based on the syntax of the token.
  418. * @private
  419. * @param {!cvoxAce.Token} token Token to speak.
  420. * @param {number} queue Queue mode.
  421. */
  422. var speakToken_ = function(token, queue) {
  423. var rule = getTokenRule(token);
  424. var value = expand(token.value, REPLACE_LIST);
  425. if (rule.replace) {
  426. value = expand(value, rule.replace);
  427. }
  428. cvox.Api.speak(value, queue, rule.prop);
  429. };
  430. /**
  431. * Speaks the character under the cursor. This is queued.
  432. * @param {!cvoxAce.Cursor} cursor Current cursor position.
  433. * @return {string} Character.
  434. */
  435. var speakChar = function(cursor) {
  436. var line = getCurrentLine(cursor);
  437. cvox.Api.speak(line[cursor.column], 1);
  438. };
  439. /**
  440. * Speaks the jump from lastCursor to currCursor. This function assumes the
  441. * jump takes place on the current line.
  442. * @param {!cvoxAce.Cursor} lastCursor Previous cursor position.
  443. * @param {!cvoxAce.Cursor} currCursor Current cursor position.
  444. */
  445. var speakDisplacement = function(lastCursor, currCursor) {
  446. var line = getCurrentLine(currCursor);
  447. /* Get the text that we jumped past. */
  448. var displace = line.substring(lastCursor.column, currCursor.column);
  449. /* Speak out loud spaces. */
  450. displace = displace.replace(/ /g, ' space ');
  451. cvox.Api.speak(displace);
  452. };
  453. /**
  454. * Speaks the word if the cursor jumped to a new word or to the beginning
  455. * of the line. Otherwise speak the charactor.
  456. * @param {!cvoxAce.Cursor} lastCursor Previous cursor position.
  457. * @param {!cvoxAce.Cursor} currCursor Current cursor position.
  458. */
  459. var speakCharOrWordOrLine = function(lastCursor, currCursor) {
  460. /* Say word only if jump. */
  461. if (Math.abs(lastCursor.column - currCursor.column) !== 1) {
  462. var currLineLength = getCurrentLine(currCursor).length;
  463. /* Speak line if jumping to beginning or end of line. */
  464. if (currCursor.column === 0 || currCursor.column === currLineLength) {
  465. speakLine(currCursor.row, 0);
  466. return;
  467. }
  468. if (isWord(currCursor)) {
  469. cvox.Api.stop();
  470. speakTokenQueue(getCurrentToken(currCursor));
  471. return;
  472. }
  473. }
  474. speakChar(currCursor);
  475. };
  476. /**
  477. * Event handler for column changes. If shouldSpeakDisplacement is on, then
  478. * we just speak displacements in row changes. Otherwise, we either speak
  479. * the character for single character movements, the word when jumping to the
  480. * next word, or the entire line if jumping to beginning or end of the line.
  481. * @param {!cvoxAce.Cursor} lastCursor Previous cursor position.
  482. * @param {!cvoxAce.Cursor} currCursor Current cursor position.
  483. */
  484. var onColumnChange = function(lastCursor, currCursor) {
  485. if (!cvoxAce.editor.selection.isEmpty()) {
  486. speakDisplacement(lastCursor, currCursor);
  487. cvox.Api.speak('selected', 1);
  488. }
  489. else if (shouldSpeakDisplacement) {
  490. speakDisplacement(lastCursor, currCursor);
  491. } else {
  492. speakCharOrWordOrLine(lastCursor, currCursor);
  493. }
  494. };
  495. /**
  496. * Event handler for cursor changes. Classify cursor changes as either row or
  497. * column changes, then delegate accordingly.
  498. * @param {!Event} evt The event.
  499. */
  500. var onCursorChange = function(evt) {
  501. /* Do not speak if cursor change was a result of text insertion. We want to
  502. * speak the text that was inserted and not where the cursor lands. */
  503. if (changed) {
  504. changed = false;
  505. return;
  506. }
  507. var currCursor = cvoxAce.editor.selection.getCursor();
  508. if (currCursor.row !== lastCursor.row) {
  509. onRowChange(currCursor);
  510. } else {
  511. onColumnChange(lastCursor, currCursor);
  512. }
  513. lastCursor = currCursor;
  514. };
  515. /**
  516. * Event handler for selection changes.
  517. * @param {!Event} evt The event.
  518. */
  519. var onSelectionChange = function(evt) {
  520. /* Assumes that when selection changes to empty, the user has unselected. */
  521. if (cvoxAce.editor.selection.isEmpty()) {
  522. cvox.Api.speak('unselected');
  523. }
  524. };
  525. /**
  526. * Event handler for source changes. We want auditory feedback for inserting
  527. * and deleting text.
  528. * @param {!Event} evt The event.
  529. */
  530. var onChange = function(delta) {
  531. switch (data.action) {
  532. case 'remove':
  533. cvox.Api.speak(data.text, 0, DELETED_PROP);
  534. /* Let the future cursor change event know it's from text change. */
  535. changed = true;
  536. break;
  537. case 'insert':
  538. cvox.Api.speak(data.text, 0);
  539. /* Let the future cursor change event know it's from text change. */
  540. changed = true;
  541. break;
  542. }
  543. };
  544. /**
  545. * Returns whether or not the annotation is new.
  546. * @param {!cvoxAce.Annotation} annot Annotation in question.
  547. * @return {boolean} Whether annot is new.
  548. */
  549. var isNewAnnotation = function(annot) {
  550. var row = annot.row;
  551. var col = annot.column;
  552. return !annotTable[row] || !annotTable[row][col];
  553. };
  554. /**
  555. * Populates the annotation table.
  556. * @param {!Array.<cvoxAce.Annotation>} annotations Array of annotations.
  557. */
  558. var populateAnnotations = function(annotations) {
  559. annotTable = {};
  560. for (var i = 0; i < annotations.length; i++) {
  561. var annotation = annotations[i];
  562. var row = annotation.row;
  563. var col = annotation.column;
  564. if (!annotTable[row]) {
  565. annotTable[row] = {};
  566. }
  567. annotTable[row][col] = annotation;
  568. }
  569. };
  570. /**
  571. * Event handler for annotation changes. We want to notify the user when an
  572. * a new annotation appears.
  573. * @param {!Event} evt Event.
  574. */
  575. var onAnnotationChange = function(evt) {
  576. var annotations = cvoxAce.editor.getSession().getAnnotations();
  577. var newAnnotations = annotations.filter(isNewAnnotation);
  578. if (newAnnotations.length > 0) {
  579. cvox.Api.playEarcon(ERROR_EARCON);
  580. }
  581. populateAnnotations(annotations);
  582. };
  583. /**
  584. * Speak annotation.
  585. * @param {!cvoxAce.Annotation} annot Annotation to speak.
  586. */
  587. var speakAnnot = function(annot) {
  588. var annotText = annot.type + ' ' + annot.text + ' on ' +
  589. rowColToString(annot.row, annot.column);
  590. annotText = annotText.replace(';', 'semicolon');
  591. cvox.Api.speak(annotText, 1);
  592. };
  593. /**
  594. * Speak annotations in a row.
  595. * @param {number} row Row of annotations to speak.
  596. */
  597. var speakAnnotsByRow = function(row) {
  598. var annots = annotTable[row];
  599. for (var col in annots) {
  600. speakAnnot(annots[col]);
  601. }
  602. };
  603. /**
  604. * Get a string representation of a row and column.
  605. * @param {boolean} row Zero indexed row.
  606. * @param {boolean} col Zero indexed column.
  607. * @return {string} Row and column to be spoken.
  608. */
  609. var rowColToString = function(row, col) {
  610. return 'row ' + (row + 1) + ' column ' + (col + 1);
  611. };
  612. /**
  613. * Speaks the row and column.
  614. */
  615. var speakCurrRowAndCol = function() {
  616. cvox.Api.speak(rowColToString(lastCursor.row, lastCursor.column));
  617. };
  618. /**
  619. * Speaks all annotations.
  620. */
  621. var speakAllAnnots = function() {
  622. for (var row in annotTable) {
  623. speakAnnotsByRow(row);
  624. }
  625. };
  626. /**
  627. * Speak the vim mode. If no vim mode, this function does nothing.
  628. */
  629. var speakMode = function() {
  630. if (!isVimMode()) {
  631. return;
  632. }
  633. switch (cvoxAce.editor.keyBinding.$data.state) {
  634. case INSERT_MODE_STATE:
  635. cvox.Api.speak('Insert mode');
  636. break;
  637. case COMMAND_MODE_STATE:
  638. cvox.Api.speak('Command mode');
  639. break;
  640. }
  641. };
  642. /**
  643. * Toggle speak location.
  644. */
  645. var toggleSpeakRowLocation = function() {
  646. shouldSpeakRowLocation = !shouldSpeakRowLocation;
  647. /* Auditory feedback of the change. */
  648. if (shouldSpeakRowLocation) {
  649. cvox.Api.speak('Speak location on row change enabled.');
  650. } else {
  651. cvox.Api.speak('Speak location on row change disabled.');
  652. }
  653. };
  654. /**
  655. * Toggle speak displacement.
  656. */
  657. var toggleSpeakDisplacement = function() {
  658. shouldSpeakDisplacement = !shouldSpeakDisplacement;
  659. /* Auditory feedback of the change. */
  660. if (shouldSpeakDisplacement) {
  661. cvox.Api.speak('Speak displacement on column changes.');
  662. } else {
  663. cvox.Api.speak('Speak current character or word on column changes.');
  664. }
  665. };
  666. /**
  667. * Event handler for key down events. Gets the right shortcut from the map,
  668. * and calls the associated function.
  669. * @param {!Event} evt Keyboard event.
  670. */
  671. var onKeyDown = function(evt) {
  672. if (evt.ctrlKey && evt.shiftKey) {
  673. var shortcut = keyCodeToShortcutMap[evt.keyCode];
  674. if (shortcut) {
  675. shortcut.func();
  676. }
  677. }
  678. };
  679. /**
  680. * Event handler for status change events. Auditory feedback of changing
  681. * between vim states.
  682. * @param {!Event} evt Change status event.
  683. * @param {!Object} editor Editor state.
  684. */
  685. var onChangeStatus = function(evt, editor) {
  686. if (!isVimMode()) {
  687. return;
  688. }
  689. var state = editor.keyBinding.$data.state;
  690. if (state === vimState) {
  691. /* State hasn't changed, do nothing. */
  692. return;
  693. }
  694. switch (state) {
  695. case INSERT_MODE_STATE:
  696. cvox.Api.playEarcon(MODE_SWITCH_EARCON);
  697. /* When in insert mode, we want to speak out keys as feedback. */
  698. cvox.Api.setKeyEcho(true);
  699. break;
  700. case COMMAND_MODE_STATE:
  701. cvox.Api.playEarcon(MODE_SWITCH_EARCON);
  702. /* When in command mode, we want don't speak out keys because those keys
  703. * are not being inserted in the document. */
  704. cvox.Api.setKeyEcho(false);
  705. break;
  706. }
  707. vimState = state;
  708. };
  709. /**
  710. * Handles context menu events. This is a ChromeVox feature where hitting
  711. * the shortcut ChromeVox + comma will open up a search bar where you can
  712. * type in various commands. All keyboard shortcuts are also commands that
  713. * can be invoked. This handles the event that ChromeVox sends to the page.
  714. * @param {Event} evt Event received.
  715. */
  716. var contextMenuHandler = function(evt) {
  717. var cmd = evt.detail['customCommand'];
  718. var shortcut = cmdToShortcutMap[cmd];
  719. if (shortcut) {
  720. shortcut.func();
  721. /* ChromeVox will bring focus to an element near the cursor instead of the
  722. * text input. */
  723. cvoxAce.editor.focus();
  724. }
  725. };
  726. /**
  727. * Initialize the ChromeVox context menu.
  728. */
  729. var initContextMenu = function() {
  730. var ACTIONS = SHORTCUTS.map(function(shortcut) {
  731. return {
  732. desc: shortcut.desc + getKeyShortcutString(shortcut.keyCode),
  733. cmd: shortcut.cmd
  734. };
  735. });
  736. /* Attach ContextMenuActions. */
  737. var body = document.querySelector('body');
  738. body.setAttribute('contextMenuActions', JSON.stringify(ACTIONS));
  739. /* Listen for ContextMenu events. */
  740. body.addEventListener('ATCustomEvent', contextMenuHandler, true);
  741. };
  742. /**
  743. * Event handler for find events. When there is a match, we want to speak the
  744. * line we are now at. Otherwise, we want to notify the user there was no
  745. * match
  746. * @param {!Event} evt The event.
  747. */
  748. var onFindSearchbox = function(evt) {
  749. if (evt.match) {
  750. /* There is still a match! Speak the line. */
  751. speakLine(lastCursor.row, 0);
  752. } else {
  753. /* No match, give auditory feedback! */
  754. cvox.Api.playEarcon(NO_MATCH_EARCON);
  755. }
  756. };
  757. /**
  758. * Focus to text input.
  759. */
  760. var focus = function() {
  761. cvoxAce.editor.focus();
  762. };
  763. /**
  764. * Shortcut definitions.
  765. */
  766. var SHORTCUTS = [
  767. {
  768. /* 1 key. */
  769. keyCode: 49,
  770. func: function() {
  771. speakAnnotsByRow(lastCursor.row);
  772. },
  773. cmd: Command.SPEAK_ANNOT,
  774. desc: 'Speak annotations on line'
  775. },
  776. {
  777. /* 2 key. */
  778. keyCode: 50,
  779. func: speakAllAnnots,
  780. cmd: Command.SPEAK_ALL_ANNOTS,
  781. desc: 'Speak all annotations'
  782. },
  783. {
  784. /* 3 key. */
  785. keyCode: 51,
  786. func: speakMode,
  787. cmd: Command.SPEAK_MODE,
  788. desc: 'Speak Vim mode'
  789. },
  790. {
  791. /* 4 key. */
  792. keyCode: 52,
  793. func: toggleSpeakRowLocation,
  794. cmd: Command.TOGGLE_LOCATION,
  795. desc: 'Toggle speak row location'
  796. },
  797. {
  798. /* 5 key. */
  799. keyCode: 53,
  800. func: speakCurrRowAndCol,
  801. cmd: Command.SPEAK_ROW_COL,
  802. desc: 'Speak row and column'
  803. },
  804. {
  805. /* 6 key. */
  806. keyCode: 54,
  807. func: toggleSpeakDisplacement,
  808. cmd: Command.TOGGLE_DISPLACEMENT,
  809. desc: 'Toggle speak displacement'
  810. },
  811. {
  812. /* 7 key. */
  813. keyCode: 55,
  814. func: focus,
  815. cmd: Command.FOCUS_TEXT,
  816. desc: 'Focus text'
  817. }
  818. ];
  819. /**
  820. * Event handler for focus events.
  821. */
  822. var onFocus = function() {
  823. cvoxAce.editor = editor;
  824. /* Set up listeners. */
  825. editor.getSession().selection.on('changeCursor', onCursorChange);
  826. editor.getSession().selection.on('changeSelection', onSelectionChange);
  827. editor.getSession().on('change', onChange);
  828. editor.getSession().on('changeAnnotation', onAnnotationChange);
  829. editor.on('changeStatus', onChangeStatus);
  830. editor.on('findSearchBox', onFindSearchbox);
  831. editor.container.addEventListener('keydown', onKeyDown);
  832. lastCursor = editor.selection.getCursor();
  833. };
  834. /**
  835. * Initialize the theme.
  836. * @param {Object} editor Editor to use.
  837. */
  838. var init = function(editor) {
  839. onFocus();
  840. /* Construct maps. */
  841. SHORTCUTS.forEach(function(shortcut) {
  842. keyCodeToShortcutMap[shortcut.keyCode] = shortcut;
  843. cmdToShortcutMap[shortcut.cmd] = shortcut;
  844. });
  845. editor.on('focus', onFocus);
  846. /* Assume we start in command mode if vim. */
  847. if (isVimMode()) {
  848. cvox.Api.setKeyEcho(false);
  849. }
  850. initContextMenu();
  851. };
  852. /**
  853. * Returns if cvox exists, and the api exists.
  854. * @return {boolean} Whether not Cvox Api exists.
  855. */
  856. function cvoxApiExists() {
  857. return (typeof(cvox) !== 'undefined') && cvox && cvox.Api;
  858. }
  859. /**
  860. * Number of tries for Cvox loading.
  861. * @type {number}
  862. */
  863. var tries = 0;
  864. /**
  865. * Max number of tries to watch for Cvox loading.
  866. * @type {number}
  867. */
  868. var MAX_TRIES = 15;
  869. /**
  870. * Check for ChromeVox load.
  871. * @param {Object} editor Editor to use.
  872. */
  873. function watchForCvoxLoad(editor) {
  874. if (cvoxApiExists()) {
  875. init(editor);
  876. } else {
  877. tries++;
  878. if (tries >= MAX_TRIES) {
  879. return;
  880. }
  881. window.setTimeout(watchForCvoxLoad, 500, editor);
  882. }
  883. }
  884. var Editor = require('../editor').Editor;
  885. require('../config').defineOptions(Editor.prototype, 'editor', {
  886. enableChromevoxEnhancements: {
  887. set: function(val) {
  888. if (val) {
  889. watchForCvoxLoad(this);
  890. }
  891. },
  892. value: true // turn it on by default or check for window.cvox
  893. }
  894. });
  895. });