1 // Copyright (c) 2012 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 // TODO(kochi): Generalize the notification as a component and put it 6 // in js/cr/ui/notification.js . 7 8 cr.define('options', function() { 9 /** @const */ var OptionsPage = options.OptionsPage; 10 /** @const */ var LanguageList = options.LanguageList; 11 12 // Some input methods like Chinese Pinyin have config pages. 13 // This is the map of the input method names to their config page names. 14 /** @const */ var INPUT_METHOD_ID_TO_CONFIG_PAGE_NAME = { 15 'mozc': 'languageMozc', 16 'mozc-chewing': 'languageChewing', 17 'mozc-dv': 'languageMozc', 18 'mozc-hangul': 'languageHangul', 19 'mozc-jp': 'languageMozc', 20 'pinyin': 'languagePinyin', 21 'pinyin-dv': 'languagePinyin', 22 }; 23 24 /** 25 * Spell check dictionary download status. 26 * @type {Enum} 27 */ 28 /** @const*/ var DOWNLOAD_STATUS = { 29 IN_PROGRESS: 1, 30 FAILED: 2 31 }; 32 33 /** 34 * The preference is a boolean that enables/disables spell checking. 35 * @type {string} 36 * @const 37 */ 38 var ENABLE_SPELL_CHECK_PREF = 'browser.enable_spellchecking'; 39 40 /** 41 * The preference is a CSV string that describes preload engines 42 * (i.e. active input methods). 43 * @type {string} 44 * @const 45 */ 46 var PRELOAD_ENGINES_PREF = 'settings.language.preload_engines'; 47 48 /** 49 * The preference that lists the extension IMEs that are enabled in the 50 * language menu. 51 * @type {string} 52 * @const 53 */ 54 var ENABLED_EXTENSION_IME_PREF = 'settings.language.enabled_extension_imes'; 55 56 /** 57 * The preference that lists the languages which are not translated. 58 * @type {string} 59 * @const 60 */ 61 var TRANSLATE_BLOCKED_LANGUAGES_PREF = 'translate_blocked_languages'; 62 63 /** 64 * The preference key that is a string that describes the spell check 65 * dictionary language, like "en-US". 66 * @type {string} 67 * @const 68 */ 69 var SPELL_CHECK_DICTIONARY_PREF = 'spellcheck.dictionary'; 70 71 ///////////////////////////////////////////////////////////////////////////// 72 // LanguageOptions class: 73 74 /** 75 * Encapsulated handling of ChromeOS language options page. 76 * @constructor 77 */ 78 function LanguageOptions(model) { 79 OptionsPage.call(this, 'languages', 80 loadTimeData.getString('languagePageTabTitle'), 81 'languagePage'); 82 } 83 84 cr.addSingletonGetter(LanguageOptions); 85 86 // Inherit LanguageOptions from OptionsPage. 87 LanguageOptions.prototype = { 88 __proto__: OptionsPage.prototype, 89 90 /* For recording the prospective language (the next locale after relaunch). 91 * @type {?string} 92 * @private 93 */ 94 prospectiveUiLanguageCode_: null, 95 96 /* 97 * Map from language code to spell check dictionary download status for that 98 * language. 99 * @type {Array} 100 * @private 101 */ 102 spellcheckDictionaryDownloadStatus_: [], 103 104 /** 105 * Number of times a spell check dictionary download failed. 106 * @type {int} 107 * @private 108 */ 109 spellcheckDictionaryDownloadFailures_: 0, 110 111 /** 112 * The list of preload engines, like ['mozc', 'pinyin']. 113 * @type {Array} 114 * @private 115 */ 116 preloadEngines_: [], 117 118 /** 119 * The list of extension IMEs that are enabled out of the language menu. 120 * @type {Array} 121 * @private 122 */ 123 enabledExtensionImes_: [], 124 125 /** 126 * The list of the languages which is not translated. 127 * @type {Array} 128 * @private 129 */ 130 translateBlockedLanguages_: [], 131 132 /** 133 * The list of the languages supported by Translate server 134 * @type {Array} 135 * @private 136 */ 137 translateSupportedLanguages_: [], 138 139 /** 140 * The preference is a string that describes the spell check dictionary 141 * language, like "en-US". 142 * @type {string} 143 * @private 144 */ 145 spellCheckDictionary_: '', 146 147 /** 148 * The map of language code to input method IDs, like: 149 * {'ja': ['mozc', 'mozc-jp'], 'zh-CN': ['pinyin'], ...} 150 * @type {Object} 151 * @private 152 */ 153 languageCodeToInputMethodIdsMap_: {}, 154 155 /** 156 * Initializes LanguageOptions page. 157 * Calls base class implementation to start preference initialization. 158 */ 159 initializePage: function() { 160 OptionsPage.prototype.initializePage.call(this); 161 162 var languageOptionsList = $('language-options-list'); 163 LanguageList.decorate(languageOptionsList); 164 165 languageOptionsList.addEventListener('change', 166 this.handleLanguageOptionsListChange_.bind(this)); 167 languageOptionsList.addEventListener('save', 168 this.handleLanguageOptionsListSave_.bind(this)); 169 170 this.prospectiveUiLanguageCode_ = 171 loadTimeData.getString('prospectiveUiLanguageCode'); 172 this.addEventListener('visibleChange', 173 this.handleVisibleChange_.bind(this)); 174 175 if (cr.isChromeOS) { 176 $('chewing-confirm').onclick = $('hangul-confirm').onclick = 177 $('mozc-confirm').onclick = $('pinyin-confirm').onclick = 178 OptionsPage.closeOverlay.bind(OptionsPage); 179 180 this.initializeInputMethodList_(); 181 this.initializeLanguageCodeToInputMethodIdsMap_(); 182 } 183 184 var checkbox = $('dont-translate-in-this-language'); 185 checkbox.addEventListener('click', 186 this.handleDontTranslateCheckboxClick_.bind(this)); 187 188 Preferences.getInstance().addEventListener( 189 TRANSLATE_BLOCKED_LANGUAGES_PREF, 190 this.handleTranslateBlockedLanguagesPrefChange_.bind(this)); 191 Preferences.getInstance().addEventListener(SPELL_CHECK_DICTIONARY_PREF, 192 this.handleSpellCheckDictionaryPrefChange_.bind(this)); 193 this.translateSupportedLanguages_ = 194 loadTimeData.getValue('translateSupportedLanguages'); 195 196 // Set up add button. 197 $('language-options-add-button').onclick = function(e) { 198 // Add the language without showing the overlay if it's specified in 199 // the URL hash (ex. lang_add=ja). Used for automated testing. 200 var match = document.location.hash.match(/\blang_add=([\w-]+)/); 201 if (match) { 202 var addLanguageCode = match[1]; 203 $('language-options-list').addLanguage(addLanguageCode); 204 this.addBlockedLanguage_(addLanguageCode); 205 } else { 206 OptionsPage.navigateToPage('addLanguage'); 207 } 208 }.bind(this); 209 210 if (!cr.isMac) { 211 // Set up the button for editing custom spelling dictionary. 212 $('edit-dictionary-button').onclick = function(e) { 213 OptionsPage.navigateToPage('editDictionary'); 214 }; 215 $('dictionary-download-retry-button').onclick = function(e) { 216 chrome.send('retryDictionaryDownload'); 217 }; 218 } 219 220 // Listen to add language dialog ok button. 221 $('add-language-overlay-ok-button').addEventListener( 222 'click', this.handleAddLanguageOkButtonClick_.bind(this)); 223 224 if (!cr.isChromeOS) { 225 // Show experimental features if enabled. 226 if (loadTimeData.getBoolean('enableSpellingAutoCorrect')) 227 $('auto-spell-correction-option').hidden = false; 228 229 // Handle spell check enable/disable. 230 if (!cr.isMac) { 231 Preferences.getInstance().addEventListener( 232 ENABLE_SPELL_CHECK_PREF, 233 this.updateEnableSpellCheck_.bind(this)); 234 } 235 } 236 237 // Handle clicks on "Use this language for spell checking" button. 238 if (!cr.isMac) { 239 var spellCheckLanguageButton = getRequiredElement( 240 'language-options-spell-check-language-button'); 241 spellCheckLanguageButton.addEventListener( 242 'click', 243 this.handleSpellCheckLanguageButtonClick_.bind(this)); 244 } 245 246 if (cr.isChromeOS) { 247 $('language-options-ui-restart-button').onclick = function() { 248 chrome.send('uiLanguageRestart'); 249 }; 250 } 251 252 $('language-confirm').onclick = 253 OptionsPage.closeOverlay.bind(OptionsPage); 254 }, 255 256 /** 257 * Initializes the input method list. 258 */ 259 initializeInputMethodList_: function() { 260 var inputMethodList = $('language-options-input-method-list'); 261 var inputMethodPrototype = $('language-options-input-method-template'); 262 263 // Add all input methods, but make all of them invisible here. We'll 264 // change the visibility in handleLanguageOptionsListChange_() based 265 // on the selected language. Note that we only have less than 100 266 // input methods, so creating DOM nodes at once here should be ok. 267 this.appendInputMethodElement_(loadTimeData.getValue('inputMethodList')); 268 this.appendInputMethodElement_(loadTimeData.getValue('extensionImeList')); 269 this.appendComponentExtensionIme_( 270 loadTimeData.getValue('componentExtensionImeList')); 271 272 // Listen to pref change once the input method list is initialized. 273 Preferences.getInstance().addEventListener( 274 PRELOAD_ENGINES_PREF, 275 this.handlePreloadEnginesPrefChange_.bind(this)); 276 Preferences.getInstance().addEventListener( 277 ENABLED_EXTENSION_IME_PREF, 278 this.handleEnabledExtensionsPrefChange_.bind(this)); 279 }, 280 281 /** 282 * Appends input method lists based on component extension ime list. 283 * @param {!Array} componentExtensionImeList A list of input method 284 * descriptors. 285 * @private 286 */ 287 appendComponentExtensionIme_: function(componentExtensionImeList) { 288 this.appendInputMethodElement_(componentExtensionImeList); 289 290 for (var i = 0; i < componentExtensionImeList.length; i++) { 291 var inputMethod = componentExtensionImeList[i]; 292 for (var languageCode in inputMethod.languageCodeSet) { 293 if (languageCode in this.languageCodeToInputMethodIdsMap_) { 294 this.languageCodeToInputMethodIdsMap_[languageCode].push( 295 inputMethod.id); 296 } else { 297 this.languageCodeToInputMethodIdsMap_[languageCode] = 298 [inputMethod.id]; 299 } 300 } 301 } 302 }, 303 304 /** 305 * Appends input methods into input method list. 306 * @param {!Array} inputMethods A list of input method descriptors. 307 * @private 308 */ 309 appendInputMethodElement_: function(inputMethods) { 310 var inputMethodList = $('language-options-input-method-list'); 311 var inputMethodTemplate = $('language-options-input-method-template'); 312 313 for (var i = 0; i < inputMethods.length; i++) { 314 var inputMethod = inputMethods[i]; 315 var element = inputMethodTemplate.cloneNode(true); 316 element.id = ''; 317 element.languageCodeSet = inputMethod.languageCodeSet; 318 319 var input = element.querySelector('input'); 320 input.inputMethodId = inputMethod.id; 321 var span = element.querySelector('span'); 322 span.textContent = inputMethod.displayName; 323 324 // Add the configure button if the config page is present for this 325 // input method. 326 if (inputMethod.id in INPUT_METHOD_ID_TO_CONFIG_PAGE_NAME) { 327 var pageName = INPUT_METHOD_ID_TO_CONFIG_PAGE_NAME[inputMethod.id]; 328 var button = this.createConfigureInputMethodButton_(inputMethod.id, 329 pageName); 330 element.appendChild(button); 331 } 332 333 if (inputMethod.optionsPage) { 334 var button = document.createElement('button'); 335 button.textContent = loadTimeData.getString('configure'); 336 button.inputMethodId = inputMethod.id; 337 button.onclick = function(optionsPage, e) { 338 window.open(optionsPage); 339 }.bind(this, inputMethod.optionsPage); 340 element.appendChild(button); 341 } 342 343 // Listen to user clicks. 344 input.addEventListener('click', 345 this.handleCheckboxClick_.bind(this)); 346 inputMethodList.appendChild(element); 347 } 348 }, 349 350 /** 351 * Creates a configure button for the given input method ID. 352 * @param {string} inputMethodId Input method ID (ex. "pinyin"). 353 * @param {string} pageName Name of the config page (ex. "languagePinyin"). 354 * @private 355 */ 356 createConfigureInputMethodButton_: function(inputMethodId, pageName) { 357 var button = document.createElement('button'); 358 button.textContent = loadTimeData.getString('configure'); 359 button.onclick = function(e) { 360 // Prevent the default action (i.e. changing the checked property 361 // of the checkbox). The button click here should not be handled 362 // as checkbox click. 363 e.preventDefault(); 364 chrome.send('inputMethodOptionsOpen', [inputMethodId]); 365 OptionsPage.navigateToPage(pageName); 366 }; 367 return button; 368 }, 369 370 /** 371 * Adds a language to the preference 'translate_blocked_languages'. If 372 * |langCode| is already added, nothing happens. |langCode| is converted 373 * to a Translate language synonym before added. 374 * @param {string} langCode A language code like 'en' 375 * @private 376 */ 377 addBlockedLanguage_: function(langCode) { 378 langCode = this.convertLangCodeForTranslation_(langCode); 379 if (this.translateBlockedLanguages_.indexOf(langCode) == -1) { 380 this.translateBlockedLanguages_.push(langCode); 381 Preferences.setListPref(TRANSLATE_BLOCKED_LANGUAGES_PREF, 382 this.translateBlockedLanguages_, true); 383 } 384 }, 385 386 /** 387 * Removes a language from the preference 'translate_blocked_languages'. 388 * If |langCode| doesn't exist in the preference, nothing happens. 389 * |langCode| is converted to a Translate language synonym before removed. 390 * @param {string} langCode A language code like 'en' 391 * @private 392 */ 393 removeBlockedLanguage_: function(langCode) { 394 langCode = this.convertLangCodeForTranslation_(langCode); 395 if (this.translateBlockedLanguages_.indexOf(langCode) != -1) { 396 this.translateBlockedLanguages_ = 397 this.translateBlockedLanguages_.filter( 398 function(langCodeNotTranslated) { 399 return langCodeNotTranslated != langCode; 400 }); 401 Preferences.setListPref(TRANSLATE_BLOCKED_LANGUAGES_PREF, 402 this.translateBlockedLanguages_, true); 403 } 404 }, 405 406 /** 407 * Handles OptionsPage's visible property change event. 408 * @param {Event} e Property change event. 409 * @private 410 */ 411 handleVisibleChange_: function(e) { 412 if (this.visible) { 413 $('language-options-list').redraw(); 414 chrome.send('languageOptionsOpen'); 415 } 416 }, 417 418 /** 419 * Handles languageOptionsList's change event. 420 * @param {Event} e Change event. 421 * @private 422 */ 423 handleLanguageOptionsListChange_: function(e) { 424 var languageOptionsList = $('language-options-list'); 425 var languageCode = languageOptionsList.getSelectedLanguageCode(); 426 427 // If there's no selection, just return. 428 if (!languageCode) 429 return; 430 431 // Select the language if it's specified in the URL hash (ex. lang=ja). 432 // Used for automated testing. 433 var match = document.location.hash.match(/\blang=([\w-]+)/); 434 if (match) { 435 var specifiedLanguageCode = match[1]; 436 if (languageOptionsList.selectLanguageByCode(specifiedLanguageCode)) { 437 languageCode = specifiedLanguageCode; 438 } 439 } 440 441 this.updateDontTranslateCheckbox_(languageCode); 442 443 if (cr.isWindows || cr.isChromeOS) 444 this.updateUiLanguageButton_(languageCode); 445 446 if (!cr.isMac) { 447 this.updateSelectedLanguageName_(languageCode); 448 this.updateSpellCheckLanguageButton_(languageCode); 449 } 450 451 if (cr.isChromeOS) 452 this.updateInputMethodList_(languageCode); 453 454 this.updateLanguageListInAddLanguageOverlay_(); 455 }, 456 457 /** 458 * Happens when a user changes back to the language they're currently using. 459 */ 460 currentLocaleWasReselected: function() { 461 this.updateUiLanguageButton_( 462 loadTimeData.getString('currentUiLanguageCode')); 463 }, 464 465 /** 466 * Handles languageOptionsList's save event. 467 * @param {Event} e Save event. 468 * @private 469 */ 470 handleLanguageOptionsListSave_: function(e) { 471 if (cr.isChromeOS) { 472 // Sort the preload engines per the saved languages before save. 473 this.preloadEngines_ = this.sortPreloadEngines_(this.preloadEngines_); 474 this.savePreloadEnginesPref_(); 475 } 476 }, 477 478 /** 479 * Sorts preloadEngines_ by languageOptionsList's order. 480 * @param {Array} preloadEngines List of preload engines. 481 * @return {Array} Returns sorted preloadEngines. 482 * @private 483 */ 484 sortPreloadEngines_: function(preloadEngines) { 485 // For instance, suppose we have two languages and associated input 486 // methods: 487 // 488 // - Korean: hangul 489 // - Chinese: pinyin 490 // 491 // The preloadEngines preference should look like "hangul,pinyin". 492 // If the user reverse the order, the preference should be reorderd 493 // to "pinyin,hangul". 494 var languageOptionsList = $('language-options-list'); 495 var languageCodes = languageOptionsList.getLanguageCodes(); 496 497 // Convert the list into a dictonary for simpler lookup. 498 var preloadEngineSet = {}; 499 for (var i = 0; i < preloadEngines.length; i++) { 500 preloadEngineSet[preloadEngines[i]] = true; 501 } 502 503 // Create the new preload engine list per the language codes. 504 var newPreloadEngines = []; 505 for (var i = 0; i < languageCodes.length; i++) { 506 var languageCode = languageCodes[i]; 507 var inputMethodIds = this.languageCodeToInputMethodIdsMap_[ 508 languageCode]; 509 if (!inputMethodIds) 510 continue; 511 512 // Check if we have active input methods associated with the language. 513 for (var j = 0; j < inputMethodIds.length; j++) { 514 var inputMethodId = inputMethodIds[j]; 515 if (inputMethodId in preloadEngineSet) { 516 // If we have, add it to the new engine list. 517 newPreloadEngines.push(inputMethodId); 518 // And delete it from the set. This is necessary as one input 519 // method can be associated with more than one language thus 520 // we should avoid having duplicates in the new list. 521 delete preloadEngineSet[inputMethodId]; 522 } 523 } 524 } 525 526 return newPreloadEngines; 527 }, 528 529 /** 530 * Initializes the map of language code to input method IDs. 531 * @private 532 */ 533 initializeLanguageCodeToInputMethodIdsMap_: function() { 534 var inputMethodList = loadTimeData.getValue('inputMethodList'); 535 for (var i = 0; i < inputMethodList.length; i++) { 536 var inputMethod = inputMethodList[i]; 537 for (var languageCode in inputMethod.languageCodeSet) { 538 if (languageCode in this.languageCodeToInputMethodIdsMap_) { 539 this.languageCodeToInputMethodIdsMap_[languageCode].push( 540 inputMethod.id); 541 } else { 542 this.languageCodeToInputMethodIdsMap_[languageCode] = 543 [inputMethod.id]; 544 } 545 } 546 } 547 }, 548 549 /** 550 * Updates the currently selected language name. 551 * @param {string} languageCode Language code (ex. "fr"). 552 * @private 553 */ 554 updateSelectedLanguageName_: function(languageCode) { 555 var languageInfo = LanguageList.getLanguageInfoFromLanguageCode( 556 languageCode); 557 var languageDisplayName = languageInfo.displayName; 558 var languageNativeDisplayName = languageInfo.nativeDisplayName; 559 var textDirection = languageInfo.textDirection; 560 561 // If the native name is different, add it. 562 if (languageDisplayName != languageNativeDisplayName) { 563 languageDisplayName += ' - ' + languageNativeDisplayName; 564 } 565 566 // Update the currently selected language name. 567 var languageName = $('language-options-language-name'); 568 languageName.textContent = languageDisplayName; 569 languageName.dir = textDirection; 570 }, 571 572 /** 573 * Updates the UI language button. 574 * @param {string} languageCode Language code (ex. "fr"). 575 * @private 576 */ 577 updateUiLanguageButton_: function(languageCode) { 578 var uiLanguageButton = $('language-options-ui-language-button'); 579 var uiLanguageMessage = $('language-options-ui-language-message'); 580 var uiLanguageNotification = $('language-options-ui-notification-bar'); 581 582 // Remove the event listener and add it back if useful. 583 uiLanguageButton.onclick = null; 584 585 // Unhide the language button every time, as it could've been previously 586 // hidden by a language change. 587 uiLanguageButton.hidden = false; 588 589 if (languageCode == this.prospectiveUiLanguageCode_) { 590 uiLanguageMessage.textContent = 591 loadTimeData.getString('isDisplayedInThisLanguage'); 592 showMutuallyExclusiveNodes( 593 [uiLanguageButton, uiLanguageMessage, uiLanguageNotification], 1); 594 } else if (languageCode in loadTimeData.getValue('uiLanguageCodeSet')) { 595 if (cr.isChromeOS && UIAccountTweaks.loggedInAsGuest()) { 596 // In the guest mode for ChromeOS, changing UI language does not make 597 // sense because it does not take effect after browser restart. 598 uiLanguageButton.hidden = true; 599 uiLanguageMessage.hidden = true; 600 } else { 601 uiLanguageButton.textContent = 602 loadTimeData.getString('displayInThisLanguage'); 603 showMutuallyExclusiveNodes( 604 [uiLanguageButton, uiLanguageMessage, uiLanguageNotification], 0); 605 uiLanguageButton.onclick = function(e) { 606 chrome.send('uiLanguageChange', [languageCode]); 607 }; 608 } 609 } else { 610 uiLanguageMessage.textContent = 611 loadTimeData.getString('cannotBeDisplayedInThisLanguage'); 612 showMutuallyExclusiveNodes( 613 [uiLanguageButton, uiLanguageMessage, uiLanguageNotification], 1); 614 } 615 }, 616 617 /** 618 * Updates the spell check language button. 619 * @param {string} languageCode Language code (ex. "fr"). 620 * @private 621 */ 622 updateSpellCheckLanguageButton_: function(languageCode) { 623 var spellCheckLanguageSection = $('language-options-spellcheck'); 624 var spellCheckLanguageButton = 625 $('language-options-spell-check-language-button'); 626 var spellCheckLanguageMessage = 627 $('language-options-spell-check-language-message'); 628 var dictionaryDownloadInProgress = 629 $('language-options-dictionary-downloading-message'); 630 var dictionaryDownloadFailed = 631 $('language-options-dictionary-download-failed-message'); 632 var dictionaryDownloadFailHelp = 633 $('language-options-dictionary-download-fail-help-message'); 634 spellCheckLanguageSection.hidden = false; 635 spellCheckLanguageMessage.hidden = true; 636 spellCheckLanguageButton.hidden = true; 637 dictionaryDownloadInProgress.hidden = true; 638 dictionaryDownloadFailed.hidden = true; 639 dictionaryDownloadFailHelp.hidden = true; 640 641 if (languageCode == this.spellCheckDictionary_) { 642 if (!(languageCode in this.spellcheckDictionaryDownloadStatus_)) { 643 spellCheckLanguageMessage.textContent = 644 loadTimeData.getString('isUsedForSpellChecking'); 645 showMutuallyExclusiveNodes( 646 [spellCheckLanguageButton, spellCheckLanguageMessage], 1); 647 } else if (this.spellcheckDictionaryDownloadStatus_[languageCode] == 648 DOWNLOAD_STATUS.IN_PROGRESS) { 649 dictionaryDownloadInProgress.hidden = false; 650 } else if (this.spellcheckDictionaryDownloadStatus_[languageCode] == 651 DOWNLOAD_STATUS.FAILED) { 652 spellCheckLanguageSection.hidden = true; 653 dictionaryDownloadFailed.hidden = false; 654 if (this.spellcheckDictionaryDownloadFailures_ > 1) 655 dictionaryDownloadFailHelp.hidden = false; 656 } 657 } else if (languageCode in 658 loadTimeData.getValue('spellCheckLanguageCodeSet')) { 659 spellCheckLanguageButton.textContent = 660 loadTimeData.getString('useThisForSpellChecking'); 661 showMutuallyExclusiveNodes( 662 [spellCheckLanguageButton, spellCheckLanguageMessage], 0); 663 spellCheckLanguageButton.languageCode = languageCode; 664 } else if (!languageCode) { 665 spellCheckLanguageButton.hidden = true; 666 spellCheckLanguageMessage.hidden = true; 667 } else { 668 spellCheckLanguageMessage.textContent = 669 loadTimeData.getString('cannotBeUsedForSpellChecking'); 670 showMutuallyExclusiveNodes( 671 [spellCheckLanguageButton, spellCheckLanguageMessage], 1); 672 } 673 }, 674 675 /** 676 * Updates the checkbox for stopping translation. 677 * @param {string} languageCode Language code (ex. "fr"). 678 * @private 679 */ 680 updateDontTranslateCheckbox_: function(languageCode) { 681 var div = $('language-options-dont-translate'); 682 683 if (!loadTimeData.getBoolean('enableTranslateSettings')) { 684 div.hidden = true; 685 return; 686 } 687 688 // Translation server supports Chinese (Transitional) and Chinese 689 // (Simplified) but not 'general' Chinese. To avoid ambiguity, we don't 690 // show this preference when general Chinese is selected. 691 if (languageCode != 'zh') { 692 div.hidden = false; 693 } else { 694 div.hidden = true; 695 return; 696 } 697 698 var dontTranslate = div.querySelector('div'); 699 var cannotTranslate = $('cannot-translate-in-this-language'); 700 var nodes = [dontTranslate, cannotTranslate]; 701 702 var convertedLangCode = this.convertLangCodeForTranslation_(languageCode); 703 if (this.translateSupportedLanguages_.indexOf(convertedLangCode) != -1) { 704 showMutuallyExclusiveNodes(nodes, 0); 705 } else { 706 showMutuallyExclusiveNodes(nodes, 1); 707 return; 708 } 709 710 var checkbox = $('dont-translate-in-this-language'); 711 712 // If the language corresponds to the default target language (in most 713 // cases, the user's locale language), "Don't Translate" checkbox should 714 // be always checked. 715 var defaultTargetLanguage = 716 loadTimeData.getString('defaultTargetLanguage'); 717 if (convertedLangCode == defaultTargetLanguage) { 718 checkbox.disabled = true; 719 checkbox.checked = true; 720 return; 721 } 722 723 checkbox.disabled = false; 724 725 var blockedLanguages = this.translateBlockedLanguages_; 726 var checked = blockedLanguages.indexOf(convertedLangCode) != -1; 727 checkbox.checked = checked; 728 }, 729 730 /** 731 * Updates the input method list. 732 * @param {string} languageCode Language code (ex. "fr"). 733 * @private 734 */ 735 updateInputMethodList_: function(languageCode) { 736 // Give one of the checkboxes or buttons focus, if it's specified in the 737 // URL hash (ex. focus=mozc). Used for automated testing. 738 var focusInputMethodId = -1; 739 var match = document.location.hash.match(/\bfocus=([\w:-]+)\b/); 740 if (match) { 741 focusInputMethodId = match[1]; 742 } 743 // Change the visibility of the input method list. Input methods that 744 // matches |languageCode| will become visible. 745 var inputMethodList = $('language-options-input-method-list'); 746 var methods = inputMethodList.querySelectorAll('.input-method'); 747 for (var i = 0; i < methods.length; i++) { 748 var method = methods[i]; 749 if (languageCode in method.languageCodeSet) { 750 method.hidden = false; 751 var input = method.querySelector('input'); 752 // Give it focus if the ID matches. 753 if (input.inputMethodId == focusInputMethodId) { 754 input.focus(); 755 } 756 } else { 757 method.hidden = true; 758 } 759 } 760 761 $('language-options-input-method-none').hidden = 762 (languageCode in this.languageCodeToInputMethodIdsMap_); 763 764 if (focusInputMethodId == 'add') { 765 $('language-options-add-button').focus(); 766 } 767 }, 768 769 /** 770 * Updates the language list in the add language overlay. 771 * @param {string} languageCode Language code (ex. "fr"). 772 * @private 773 */ 774 updateLanguageListInAddLanguageOverlay_: function(languageCode) { 775 // Change the visibility of the language list in the add language 776 // overlay. Languages that are already active will become invisible, 777 // so that users don't add the same language twice. 778 var languageOptionsList = $('language-options-list'); 779 var languageCodes = languageOptionsList.getLanguageCodes(); 780 var languageCodeSet = {}; 781 for (var i = 0; i < languageCodes.length; i++) { 782 languageCodeSet[languageCodes[i]] = true; 783 } 784 785 var addLanguageList = $('add-language-overlay-language-list'); 786 var options = addLanguageList.querySelectorAll('option'); 787 assert(options.length > 0); 788 var selectedFirstItem = false; 789 for (var i = 0; i < options.length; i++) { 790 var option = options[i]; 791 option.hidden = option.value in languageCodeSet; 792 if (!option.hidden && !selectedFirstItem) { 793 // Select first visible item, otherwise previously selected hidden 794 // item will be selected by default at the next time. 795 option.selected = true; 796 selectedFirstItem = true; 797 } 798 } 799 }, 800 801 /** 802 * Handles preloadEnginesPref change. 803 * @param {Event} e Change event. 804 * @private 805 */ 806 handlePreloadEnginesPrefChange_: function(e) { 807 var value = e.value.value; 808 this.preloadEngines_ = this.filterBadPreloadEngines_(value.split(',')); 809 this.updateCheckboxesFromPreloadEngines_(); 810 $('language-options-list').updateDeletable(); 811 }, 812 813 /** 814 * Handles enabledExtensionImePref change. 815 * @param {Event} e Change event. 816 * @private 817 */ 818 handleEnabledExtensionsPrefChange_: function(e) { 819 var value = e.value.value; 820 this.enabledExtensionImes_ = value.split(','); 821 this.updateCheckboxesFromEnabledExtensions_(); 822 }, 823 824 /** 825 * Handles don't-translate checkbox's click event. 826 * @param {Event} e Click event. 827 * @private 828 */ 829 handleDontTranslateCheckboxClick_: function(e) { 830 var checkbox = e.target; 831 var checked = checkbox.checked; 832 833 var languageOptionsList = $('language-options-list'); 834 var selectedLanguageCode = languageOptionsList.getSelectedLanguageCode(); 835 836 if (checked) 837 this.addBlockedLanguage_(selectedLanguageCode); 838 else 839 this.removeBlockedLanguage_(selectedLanguageCode); 840 }, 841 842 /** 843 * Handles input method checkbox's click event. 844 * @param {Event} e Click event. 845 * @private 846 */ 847 handleCheckboxClick_: function(e) { 848 var checkbox = e.target; 849 850 if (checkbox.inputMethodId.match(/^_ext_ime_/)) { 851 this.updateEnabledExtensionsFromCheckboxes_(); 852 this.saveEnabledExtensionPref_(); 853 return; 854 } 855 if (this.preloadEngines_.length == 1 && !checkbox.checked) { 856 // Don't allow disabling the last input method. 857 this.showNotification_( 858 loadTimeData.getString('pleaseAddAnotherInputMethod'), 859 loadTimeData.getString('okButton')); 860 checkbox.checked = true; 861 return; 862 } 863 if (checkbox.checked) { 864 chrome.send('inputMethodEnable', [checkbox.inputMethodId]); 865 } else { 866 chrome.send('inputMethodDisable', [checkbox.inputMethodId]); 867 } 868 this.updatePreloadEnginesFromCheckboxes_(); 869 this.preloadEngines_ = this.sortPreloadEngines_(this.preloadEngines_); 870 this.savePreloadEnginesPref_(); 871 }, 872 873 handleAddLanguageOkButtonClick_: function() { 874 var languagesSelect = $('add-language-overlay-language-list'); 875 var selectedIndex = languagesSelect.selectedIndex; 876 if (selectedIndex >= 0) { 877 var selection = languagesSelect.options[selectedIndex]; 878 var langCode = String(selection.value); 879 $('language-options-list').addLanguage(langCode); 880 this.addBlockedLanguage_(langCode); 881 OptionsPage.closeOverlay(); 882 } 883 }, 884 885 /** 886 * Checks if languageCode is deletable or not. 887 * @param {string} languageCode the languageCode to check for deletability. 888 */ 889 languageIsDeletable: function(languageCode) { 890 // Don't allow removing the language if it's a UI language. 891 if (languageCode == this.prospectiveUiLanguageCode_) 892 return false; 893 return (!cr.isChromeOS || 894 this.canDeleteLanguage_(languageCode)); 895 }, 896 897 /** 898 * Handles browse.enable_spellchecking change. 899 * @param {Event} e Change event. 900 * @private 901 */ 902 updateEnableSpellCheck_: function() { 903 var value = !$('enable-spell-check').checked; 904 $('language-options-spell-check-language-button').disabled = value; 905 if (!cr.IsMac) 906 $('edit-dictionary-button').hidden = value; 907 }, 908 909 /** 910 * Handles translateBlockedLanguagesPref change. 911 * @param {Event} e Change event. 912 * @private 913 */ 914 handleTranslateBlockedLanguagesPrefChange_: function(e) { 915 var languageOptionsList = $('language-options-list'); 916 var selectedLanguageCode = languageOptionsList.getSelectedLanguageCode(); 917 this.translateBlockedLanguages_ = e.value.value; 918 this.updateDontTranslateCheckbox_(selectedLanguageCode); 919 }, 920 921 /** 922 * Handles spellCheckDictionaryPref change. 923 * @param {Event} e Change event. 924 * @private 925 */ 926 handleSpellCheckDictionaryPrefChange_: function(e) { 927 var languageCode = e.value.value; 928 this.spellCheckDictionary_ = languageCode; 929 var languageOptionsList = $('language-options-list'); 930 var selectedLanguageCode = languageOptionsList.getSelectedLanguageCode(); 931 if (!cr.isMac) 932 this.updateSpellCheckLanguageButton_(selectedLanguageCode); 933 }, 934 935 /** 936 * Handles spellCheckLanguageButton click. 937 * @param {Event} e Click event. 938 * @private 939 */ 940 handleSpellCheckLanguageButtonClick_: function(e) { 941 var languageCode = e.target.languageCode; 942 // Save the preference. 943 Preferences.setStringPref(SPELL_CHECK_DICTIONARY_PREF, 944 languageCode, true); 945 chrome.send('spellCheckLanguageChange', [languageCode]); 946 }, 947 948 /** 949 * Checks whether it's possible to remove the language specified by 950 * languageCode and returns true if possible. This function returns false 951 * if the removal causes the number of preload engines to be zero. 952 * 953 * @param {string} languageCode Language code (ex. "fr"). 954 * @return {boolean} Returns true on success. 955 * @private 956 */ 957 canDeleteLanguage_: function(languageCode) { 958 // First create the set of engines to be removed from input methods 959 // associated with the language code. 960 var enginesToBeRemovedSet = {}; 961 var inputMethodIds = this.languageCodeToInputMethodIdsMap_[languageCode]; 962 963 // If this language doesn't have any input methods, it can be deleted. 964 if (!inputMethodIds) 965 return true; 966 967 for (var i = 0; i < inputMethodIds.length; i++) { 968 enginesToBeRemovedSet[inputMethodIds[i]] = true; 969 } 970 971 // Then eliminate engines that are also used for other active languages. 972 // For instance, if "xkb:us::eng" is used for both English and Filipino. 973 var languageCodes = $('language-options-list').getLanguageCodes(); 974 for (var i = 0; i < languageCodes.length; i++) { 975 // Skip the target language code. 976 if (languageCodes[i] == languageCode) { 977 continue; 978 } 979 // Check if input methods used in this language are included in 980 // enginesToBeRemovedSet. If so, eliminate these from the set, so 981 // we don't remove this time. 982 var inputMethodIdsForAnotherLanguage = 983 this.languageCodeToInputMethodIdsMap_[languageCodes[i]]; 984 if (!inputMethodIdsForAnotherLanguage) 985 continue; 986 987 for (var j = 0; j < inputMethodIdsForAnotherLanguage.length; j++) { 988 var inputMethodId = inputMethodIdsForAnotherLanguage[j]; 989 if (inputMethodId in enginesToBeRemovedSet) { 990 delete enginesToBeRemovedSet[inputMethodId]; 991 } 992 } 993 } 994 995 // Update the preload engine list with the to-be-removed set. 996 var newPreloadEngines = []; 997 for (var i = 0; i < this.preloadEngines_.length; i++) { 998 if (!(this.preloadEngines_[i] in enginesToBeRemovedSet)) { 999 newPreloadEngines.push(this.preloadEngines_[i]); 1000 } 1001 } 1002 // Don't allow this operation if it causes the number of preload 1003 // engines to be zero. 1004 return (newPreloadEngines.length > 0); 1005 }, 1006 1007 /** 1008 * Saves the enabled extension preference. 1009 * @private 1010 */ 1011 saveEnabledExtensionPref_: function() { 1012 Preferences.setStringPref(ENABLED_EXTENSION_IME_PREF, 1013 this.enabledExtensionImes_.join(','), true); 1014 }, 1015 1016 /** 1017 * Updates the checkboxes in the input method list from the enabled 1018 * extensions preference. 1019 * @private 1020 */ 1021 updateCheckboxesFromEnabledExtensions_: function() { 1022 // Convert the list into a dictonary for simpler lookup. 1023 var dictionary = {}; 1024 for (var i = 0; i < this.enabledExtensionImes_.length; i++) 1025 dictionary[this.enabledExtensionImes_[i]] = true; 1026 1027 var inputMethodList = $('language-options-input-method-list'); 1028 var checkboxes = inputMethodList.querySelectorAll('input'); 1029 for (var i = 0; i < checkboxes.length; i++) { 1030 if (checkboxes[i].inputMethodId.match(/^_ext_ime_/)) 1031 checkboxes[i].checked = (checkboxes[i].inputMethodId in dictionary); 1032 } 1033 }, 1034 1035 /** 1036 * Updates the enabled extensions preference from the checkboxes in the 1037 * input method list. 1038 * @private 1039 */ 1040 updateEnabledExtensionsFromCheckboxes_: function() { 1041 this.enabledExtensionImes_ = []; 1042 var inputMethodList = $('language-options-input-method-list'); 1043 var checkboxes = inputMethodList.querySelectorAll('input'); 1044 for (var i = 0; i < checkboxes.length; i++) { 1045 if (checkboxes[i].inputMethodId.match(/^_ext_ime_/)) { 1046 if (checkboxes[i].checked) 1047 this.enabledExtensionImes_.push(checkboxes[i].inputMethodId); 1048 } 1049 } 1050 }, 1051 1052 /** 1053 * Saves the preload engines preference. 1054 * @private 1055 */ 1056 savePreloadEnginesPref_: function() { 1057 Preferences.setStringPref(PRELOAD_ENGINES_PREF, 1058 this.preloadEngines_.join(','), true); 1059 }, 1060 1061 /** 1062 * Updates the checkboxes in the input method list from the preload 1063 * engines preference. 1064 * @private 1065 */ 1066 updateCheckboxesFromPreloadEngines_: function() { 1067 // Convert the list into a dictonary for simpler lookup. 1068 var dictionary = {}; 1069 for (var i = 0; i < this.preloadEngines_.length; i++) { 1070 dictionary[this.preloadEngines_[i]] = true; 1071 } 1072 1073 var inputMethodList = $('language-options-input-method-list'); 1074 var checkboxes = inputMethodList.querySelectorAll('input'); 1075 for (var i = 0; i < checkboxes.length; i++) { 1076 if (!checkboxes[i].inputMethodId.match(/^_ext_ime_/)) 1077 checkboxes[i].checked = (checkboxes[i].inputMethodId in dictionary); 1078 } 1079 var configureButtons = inputMethodList.querySelectorAll('button'); 1080 for (var i = 0; i < configureButtons.length; i++) { 1081 configureButtons[i].hidden = 1082 !(configureButtons[i].inputMethodId in dictionary); 1083 } 1084 }, 1085 1086 /** 1087 * Updates the preload engines preference from the checkboxes in the 1088 * input method list. 1089 * @private 1090 */ 1091 updatePreloadEnginesFromCheckboxes_: function() { 1092 this.preloadEngines_ = []; 1093 var inputMethodList = $('language-options-input-method-list'); 1094 var checkboxes = inputMethodList.querySelectorAll('input'); 1095 for (var i = 0; i < checkboxes.length; i++) { 1096 if (!checkboxes[i].inputMethodId.match(/^_ext_ime_/)) { 1097 if (checkboxes[i].checked) 1098 this.preloadEngines_.push(checkboxes[i].inputMethodId); 1099 } 1100 } 1101 var languageOptionsList = $('language-options-list'); 1102 languageOptionsList.updateDeletable(); 1103 }, 1104 1105 /** 1106 * Filters bad preload engines in case bad preload engines are 1107 * stored in the preference. Removes duplicates as well. 1108 * @param {Array} preloadEngines List of preload engines. 1109 * @private 1110 */ 1111 filterBadPreloadEngines_: function(preloadEngines) { 1112 // Convert the list into a dictonary for simpler lookup. 1113 var dictionary = {}; 1114 var list = loadTimeData.getValue('inputMethodList'); 1115 for (var i = 0; i < list.length; i++) { 1116 dictionary[list[i].id] = true; 1117 } 1118 1119 var enabledPreloadEngines = []; 1120 var seen = {}; 1121 for (var i = 0; i < preloadEngines.length; i++) { 1122 // Check if the preload engine is present in the 1123 // dictionary, and not duplicate. Otherwise, skip it. 1124 // Component Extension IME should be handled same as preloadEngines and 1125 // "_comp_" is the special prefix of its ID. 1126 if ((preloadEngines[i] in dictionary && !(preloadEngines[i] in seen)) || 1127 /^_comp_/.test(preloadEngines[i])) { 1128 enabledPreloadEngines.push(preloadEngines[i]); 1129 seen[preloadEngines[i]] = true; 1130 } 1131 } 1132 return enabledPreloadEngines; 1133 }, 1134 1135 // TODO(kochi): This is an adapted copy from new_tab.js. 1136 // If this will go as final UI, refactor this to share the component with 1137 // new new tab page. 1138 /** 1139 * Shows notification 1140 * @private 1141 */ 1142 notificationTimeout_: null, 1143 showNotification_: function(text, actionText, opt_delay) { 1144 var notificationElement = $('notification'); 1145 var actionLink = notificationElement.querySelector('.link-color'); 1146 var delay = opt_delay || 10000; 1147 1148 function show() { 1149 window.clearTimeout(this.notificationTimeout_); 1150 notificationElement.classList.add('show'); 1151 document.body.classList.add('notification-shown'); 1152 } 1153 1154 function hide() { 1155 window.clearTimeout(this.notificationTimeout_); 1156 notificationElement.classList.remove('show'); 1157 document.body.classList.remove('notification-shown'); 1158 // Prevent tabbing to the hidden link. 1159 actionLink.tabIndex = -1; 1160 // Setting tabIndex to -1 only prevents future tabbing to it. If, 1161 // however, the user switches window or a tab and then moves back to 1162 // this tab the element may gain focus. We therefore make sure that we 1163 // blur the element so that the element focus is not restored when 1164 // coming back to this window. 1165 actionLink.blur(); 1166 } 1167 1168 function delayedHide() { 1169 this.notificationTimeout_ = window.setTimeout(hide, delay); 1170 } 1171 1172 notificationElement.firstElementChild.textContent = text; 1173 actionLink.textContent = actionText; 1174 1175 actionLink.onclick = hide; 1176 actionLink.onkeydown = function(e) { 1177 if (e.keyIdentifier == 'Enter') { 1178 hide(); 1179 } 1180 }; 1181 notificationElement.onmouseover = show; 1182 notificationElement.onmouseout = delayedHide; 1183 actionLink.onfocus = show; 1184 actionLink.onblur = delayedHide; 1185 // Enable tabbing to the link now that it is shown. 1186 actionLink.tabIndex = 0; 1187 1188 show(); 1189 delayedHide(); 1190 }, 1191 1192 onDictionaryDownloadBegin_: function(languageCode) { 1193 this.spellcheckDictionaryDownloadStatus_[languageCode] = 1194 DOWNLOAD_STATUS.IN_PROGRESS; 1195 if (!cr.isMac && 1196 languageCode == 1197 $('language-options-list').getSelectedLanguageCode()) { 1198 this.updateSpellCheckLanguageButton_(languageCode); 1199 } 1200 }, 1201 1202 onDictionaryDownloadSuccess_: function(languageCode) { 1203 delete this.spellcheckDictionaryDownloadStatus_[languageCode]; 1204 this.spellcheckDictionaryDownloadFailures_ = 0; 1205 if (!cr.isMac && 1206 languageCode == 1207 $('language-options-list').getSelectedLanguageCode()) { 1208 this.updateSpellCheckLanguageButton_(languageCode); 1209 } 1210 }, 1211 1212 onDictionaryDownloadFailure_: function(languageCode) { 1213 this.spellcheckDictionaryDownloadStatus_[languageCode] = 1214 DOWNLOAD_STATUS.FAILED; 1215 this.spellcheckDictionaryDownloadFailures_++; 1216 if (!cr.isMac && 1217 languageCode == 1218 $('language-options-list').getSelectedLanguageCode()) { 1219 this.updateSpellCheckLanguageButton_(languageCode); 1220 } 1221 }, 1222 1223 /* 1224 * Converts the language code for Translation. There are some differences 1225 * between the language set for Translation and that for Accept-Language. 1226 * @param {string} languageCode The language code like 'fr'. 1227 * @return {string} The converted language code. 1228 * @private 1229 */ 1230 convertLangCodeForTranslation_: function(languageCode) { 1231 var tokens = languageCode.split('-'); 1232 var main = tokens[0]; 1233 1234 // See also: chrome/renderer/translate/translate_helper.cc. 1235 var synonyms = { 1236 'nb': 'no', 1237 'he': 'iw', 1238 'jv': 'jw', 1239 'fil': 'tl', 1240 }; 1241 1242 if (main in synonyms) { 1243 return synonyms[main]; 1244 } else if (main == 'zh') { 1245 // In Translation, general Chinese is not used, and the sub code is 1246 // necessary as a language code for Translate server. 1247 return languageCode; 1248 } 1249 1250 return main; 1251 }, 1252 }; 1253 1254 /** 1255 * Shows the node at |index| in |nodes|, hides all others. 1256 * @param {Array<HTMLElement>} nodes The nodes to be shown or hidden. 1257 * @param {number} index The index of |nodes| to show. 1258 */ 1259 function showMutuallyExclusiveNodes(nodes, index) { 1260 assert(index >= 0 && index < nodes.length); 1261 for (var i = 0; i < nodes.length; ++i) { 1262 assert(nodes[i] instanceof HTMLElement); // TODO(dbeam): Ignore null? 1263 nodes[i].hidden = i != index; 1264 } 1265 } 1266 1267 /** 1268 * Chrome callback for when the UI language preference is saved. 1269 * @param {string} languageCode The newly selected language to use. 1270 */ 1271 LanguageOptions.uiLanguageSaved = function(languageCode) { 1272 this.prospectiveUiLanguageCode_ = languageCode; 1273 1274 // If the user is no longer on the same language code, ignore. 1275 if ($('language-options-list').getSelectedLanguageCode() != languageCode) 1276 return; 1277 1278 // Special case for when a user changes to a different language, and changes 1279 // back to the same language without having restarted Chrome or logged 1280 // in/out of ChromeOS. 1281 if (languageCode == loadTimeData.getString('currentUiLanguageCode')) { 1282 LanguageOptions.getInstance().currentLocaleWasReselected(); 1283 return; 1284 } 1285 1286 // Otherwise, show a notification telling the user that their changes will 1287 // only take effect after restart. 1288 showMutuallyExclusiveNodes([$('language-options-ui-language-button'), 1289 $('language-options-ui-notification-bar')], 1); 1290 }; 1291 1292 LanguageOptions.onDictionaryDownloadBegin = function(languageCode) { 1293 LanguageOptions.getInstance().onDictionaryDownloadBegin_(languageCode); 1294 }; 1295 1296 LanguageOptions.onDictionaryDownloadSuccess = function(languageCode) { 1297 LanguageOptions.getInstance().onDictionaryDownloadSuccess_(languageCode); 1298 }; 1299 1300 LanguageOptions.onDictionaryDownloadFailure = function(languageCode) { 1301 LanguageOptions.getInstance().onDictionaryDownloadFailure_(languageCode); 1302 }; 1303 1304 LanguageOptions.onComponentManagerInitialized = function(componentImes) { 1305 LanguageOptions.getInstance().appendComponentExtensionIme_(componentImes); 1306 }; 1307 1308 // Export 1309 return { 1310 LanguageOptions: LanguageOptions 1311 }; 1312 }); 1313