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