diff options
author | Andras Becsi <andras.becsi@digia.com> | 2014-03-18 13:16:26 +0100 |
---|---|---|
committer | Frederik Gladhorn <frederik.gladhorn@digia.com> | 2014-03-20 15:55:39 +0100 |
commit | 3f0f86b0caed75241fa71c95a5d73bc0164348c5 (patch) | |
tree | 92b9fb00f2e9e90b0be2262093876d4f43b6cd13 /chromium/chrome/browser/resources/options | |
parent | e90d7c4b152c56919d963987e2503f9909a666d2 (diff) | |
download | qtwebengine-chromium-3f0f86b0caed75241fa71c95a5d73bc0164348c5.tar.gz |
Update to new stable branch 1750
This also includes an updated ninja and chromium dependencies
needed on Windows.
Change-Id: Icd597d80ed3fa4425933c9f1334c3c2e31291c42
Reviewed-by: Zoltan Arvai <zarvai@inf.u-szeged.hu>
Reviewed-by: Zeno Albisser <zeno.albisser@digia.com>
Diffstat (limited to 'chromium/chrome/browser/resources/options')
183 files changed, 30991 insertions, 0 deletions
diff --git a/chromium/chrome/browser/resources/options/2x/yellow_gear.png b/chromium/chrome/browser/resources/options/2x/yellow_gear.png Binary files differnew file mode 100644 index 00000000000..087c8a329a6 --- /dev/null +++ b/chromium/chrome/browser/resources/options/2x/yellow_gear.png diff --git a/chromium/chrome/browser/resources/options/OWNERS b/chromium/chrome/browser/resources/options/OWNERS new file mode 100644 index 00000000000..8da21baca39 --- /dev/null +++ b/chromium/chrome/browser/resources/options/OWNERS @@ -0,0 +1,3 @@ +dbeam@chromium.org +estade@chromium.org +jhawkins@chromium.org diff --git a/chromium/chrome/browser/resources/options/alert_overlay.css b/chromium/chrome/browser/resources/options/alert_overlay.css new file mode 100644 index 00000000000..b2d9960a9a1 --- /dev/null +++ b/chromium/chrome/browser/resources/options/alert_overlay.css @@ -0,0 +1,7 @@ +/* Copyright (c) 2012 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ + +#alertOverlayMessage { + width: 400px; +} diff --git a/chromium/chrome/browser/resources/options/alert_overlay.html b/chromium/chrome/browser/resources/options/alert_overlay.html new file mode 100644 index 00000000000..2c96ff14f79 --- /dev/null +++ b/chromium/chrome/browser/resources/options/alert_overlay.html @@ -0,0 +1,13 @@ +<div id="alertOverlay" class="page" hidden> + <div class="close-button"></div> + <h1 id="alertOverlayTitle"></h1> + <div class="content-area"> + <div id="alertOverlayMessage"></div> + </div> + <div class="action-area"> + <div class="button-strip"> + <button id="alertOverlayCancel" type="reset"></button> + <button id="alertOverlayOk" class="default-button" type="submit"></button> + </div> + </div> +</div> diff --git a/chromium/chrome/browser/resources/options/alert_overlay.js b/chromium/chrome/browser/resources/options/alert_overlay.js new file mode 100644 index 00000000000..aab11765e67 --- /dev/null +++ b/chromium/chrome/browser/resources/options/alert_overlay.js @@ -0,0 +1,152 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + var OptionsPage = options.OptionsPage; + + /** + * AlertOverlay class + * Encapsulated handling of a generic alert. + * @class + */ + function AlertOverlay() { + OptionsPage.call(this, 'alertOverlay', '', 'alertOverlay'); + } + + cr.addSingletonGetter(AlertOverlay); + + AlertOverlay.prototype = { + // Inherit AlertOverlay from OptionsPage. + __proto__: OptionsPage.prototype, + + /** + * Whether the page can be shown. Used to make sure the page is only + * shown via AlertOverlay.Show(), and not via the address bar. + * @private + */ + canShow_: false, + + /** + * Initialize the page. + */ + initializePage: function() { + // Call base class implementation to start preference initialization. + OptionsPage.prototype.initializePage.call(this); + + var self = this; + $('alertOverlayOk').onclick = function(event) { + self.handleOK_(); + }; + + $('alertOverlayCancel').onclick = function(event) { + self.handleCancel_(); + }; + }, + + /** @override */ + get nestingLevel() { + // AlertOverlay is special in that it is not tied to one page or overlay. + // Set the nesting level arbitrarily high so as to always be recognized as + // the top-most visible page. + return 99; + }, + + /** + * Handle the 'ok' button. Clear the overlay and call the ok callback if + * available. + * @private + */ + handleOK_: function() { + OptionsPage.closeOverlay(); + if (this.okCallback != undefined) { + this.okCallback.call(); + } + }, + + /** + * Handle the 'cancel' button. Clear the overlay and call the cancel + * callback if available. + * @private + */ + handleCancel_: function() { + OptionsPage.closeOverlay(); + if (this.cancelCallback != undefined) { + this.cancelCallback.call(); + } + }, + + /** + * The page is getting hidden. Don't let it be shown again. + */ + willHidePage: function() { + canShow_ = false; + }, + + /** @override */ + canShowPage: function() { + return this.canShow_; + }, + }; + + /** + * Show an alert overlay with the given message, button titles, and + * callbacks. + * @param {string} title The alert title to display to the user. + * @param {string} message The alert message to display to the user. + * @param {string} okTitle The title of the OK button. If undefined or empty, + * no button is shown. + * @param {string} cancelTitle The title of the cancel button. If undefined or + * empty, no button is shown. + * @param {function} okCallback A function to be called when the user presses + * the ok button. The alert window will be closed automatically. Can be + * undefined. + * @param {function} cancelCallback A function to be called when the user + * presses the cancel button. The alert window will be closed + * automatically. Can be undefined. + */ + AlertOverlay.show = function( + title, message, okTitle, cancelTitle, okCallback, cancelCallback) { + if (title != undefined) { + $('alertOverlayTitle').textContent = title; + $('alertOverlayTitle').style.display = 'block'; + } else { + $('alertOverlayTitle').style.display = 'none'; + } + + if (message != undefined) { + $('alertOverlayMessage').textContent = message; + $('alertOverlayMessage').style.display = 'block'; + } else { + $('alertOverlayMessage').style.display = 'none'; + } + + if (okTitle != undefined && okTitle != '') { + $('alertOverlayOk').textContent = okTitle; + $('alertOverlayOk').style.display = 'block'; + } else { + $('alertOverlayOk').style.display = 'none'; + } + + if (cancelTitle != undefined && cancelTitle != '') { + $('alertOverlayCancel').textContent = cancelTitle; + $('alertOverlayCancel').style.display = 'inline'; + } else { + $('alertOverlayCancel').style.display = 'none'; + } + + var alertOverlay = AlertOverlay.getInstance(); + alertOverlay.okCallback = okCallback; + alertOverlay.cancelCallback = cancelCallback; + alertOverlay.canShow_ = true; + + // Intentionally don't show the URL in the location bar as we don't want + // people trying to navigate here by hand. + OptionsPage.showPageByName('alertOverlay', false); + }; + + // Export + return { + AlertOverlay: AlertOverlay + }; +}); diff --git a/chromium/chrome/browser/resources/options/arrow_next.png b/chromium/chrome/browser/resources/options/arrow_next.png Binary files differnew file mode 100644 index 00000000000..7d626c0e8cd --- /dev/null +++ b/chromium/chrome/browser/resources/options/arrow_next.png diff --git a/chromium/chrome/browser/resources/options/autofill_edit_address_overlay.html b/chromium/chrome/browser/resources/options/autofill_edit_address_overlay.html new file mode 100644 index 00000000000..44ff77f147c --- /dev/null +++ b/chromium/chrome/browser/resources/options/autofill_edit_address_overlay.html @@ -0,0 +1,75 @@ +<div id="autofill-edit-address-overlay" class="page" hidden> + <div class="close-button"></div> + <h1 id="autofill-address-title"></h1> + <div class="content-area"> + <div> + <div id="autofill-name-labels"> + <span i18n-content="autofillFirstNameLabel"></span> + <span i18n-content="autofillMiddleNameLabel"></span> + <span i18n-content="autofillLastNameLabel"></span> + </div> + </div> + <div> + <list id="full-name-list"></list> + </div> + + <label class="settings-row"> + <div i18n-content="autofillCompanyNameLabel"></div> + <input id="company-name" type="text"> + </label> + + <label class="settings-row"> + <div i18n-content="autofillAddrLine1Label"></div> + <input id="addr-line-1" type="text"> + </label> + + <label class="settings-row"> + <div i18n-content="autofillAddrLine2Label"></div> + <input id="addr-line-2" type="text"> + </label> + + <div class="input-group settings-row"> + <label> + <div i18n-content="autofillCityLabel"></div> + <input id="city" type="text"> + </label> + + <label> + <div id="state-label"></div> + <input id="state" type="text"> + </label> + + <label> + <div id="postal-code-label"></div> + <input id="postal-code" type="text"> + </label> + </div> + + <div class="settings-row"> + <div i18n-content="autofillCountryLabel"></div> + <select id="country"></select> + </div> + + <div class="input-group settings-row"> + <div> + <div i18n-content="autofillPhoneLabel"></div> + <list id="phone-list" + i18n-values="placeholder:autofillAddPhonePlaceholder"></list> + </div> + + <div> + <div i18n-content="autofillEmailLabel"></div> + <list id="email-list" + i18n-values="placeholder:autofillAddEmailPlaceholder"></list> + </div> + </div> + + </div> + + <div class="action-area button-strip"> + <button id="autofill-edit-address-cancel-button" type="reset" + i18n-content="cancel"></button> + <button id="autofill-edit-address-apply-button" type="submit" + class="default-button" i18n-content="ok" disabled></button> + </div> +</div> diff --git a/chromium/chrome/browser/resources/options/autofill_edit_address_overlay.js b/chromium/chrome/browser/resources/options/autofill_edit_address_overlay.js new file mode 100644 index 00000000000..94b6a5d307d --- /dev/null +++ b/chromium/chrome/browser/resources/options/autofill_edit_address_overlay.js @@ -0,0 +1,301 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + /** @const */ var OptionsPage = options.OptionsPage; + /** @const */ var ArrayDataModel = cr.ui.ArrayDataModel; + + // The GUID of the loaded address. + var guid; + + /** + * AutofillEditAddressOverlay class + * Encapsulated handling of the 'Add Page' overlay page. + * @class + */ + function AutofillEditAddressOverlay() { + OptionsPage.call(this, 'autofillEditAddress', + loadTimeData.getString('autofillEditAddressTitle'), + 'autofill-edit-address-overlay'); + } + + cr.addSingletonGetter(AutofillEditAddressOverlay); + + AutofillEditAddressOverlay.prototype = { + __proto__: OptionsPage.prototype, + + /** + * Initializes the page. + */ + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + + this.createMultiValueLists_(); + + var self = this; + $('autofill-edit-address-cancel-button').onclick = function(event) { + self.dismissOverlay_(); + }; + + // TODO(jhawkins): Investigate other possible solutions. + $('autofill-edit-address-apply-button').onclick = function(event) { + // Blur active element to ensure that pending changes are committed. + if (document.activeElement) + document.activeElement.blur(); + // Blurring is delayed for list elements. Queue save and close to + // ensure that pending changes have been applied. + setTimeout(function() { + self.saveAddress_(); + self.dismissOverlay_(); + }, 0); + }; + + // Prevent 'blur' events on the OK and cancel buttons, which can trigger + // insertion of new placeholder elements. The addition of placeholders + // affects layout, which interferes with being able to click on the + // buttons. + $('autofill-edit-address-apply-button').onmousedown = function(event) { + event.preventDefault(); + }; + $('autofill-edit-address-cancel-button').onmousedown = function(event) { + event.preventDefault(); + }; + + self.guid = ''; + self.populateCountryList_(); + self.clearInputFields_(); + self.connectInputEvents_(); + }, + + /** + * Specifically catch the situations in which the overlay is cancelled + * externally (e.g. by pressing <Esc>), so that the input fields and + * GUID can be properly cleared. + * @override + */ + handleCancel: function() { + this.dismissOverlay_(); + }, + + /** + * Creates, decorates and initializes the multi-value lists for full name, + * phone, and email. + * @private + */ + createMultiValueLists_: function() { + var list = $('full-name-list'); + options.autofillOptions.AutofillNameValuesList.decorate(list); + list.autoExpands = true; + + list = $('phone-list'); + options.autofillOptions.AutofillPhoneValuesList.decorate(list); + list.autoExpands = true; + + list = $('email-list'); + options.autofillOptions.AutofillValuesList.decorate(list); + list.autoExpands = true; + }, + + /** + * Updates the data model for the list named |listName| with the values from + * |entries|. + * @param {string} listName The id of the list. + * @param {Array} entries The list of items to be added to the list. + */ + setMultiValueList_: function(listName, entries) { + // Add data entries. + var list = $(listName); + + // Add special entry for adding new values. + var augmentedList = entries.slice(); + augmentedList.push(null); + list.dataModel = new ArrayDataModel(augmentedList); + + // Update the status of the 'OK' button. + this.inputFieldChanged_(); + + list.dataModel.addEventListener('splice', + this.inputFieldChanged_.bind(this)); + list.dataModel.addEventListener('change', + this.inputFieldChanged_.bind(this)); + }, + + /** + * Clears any uncommitted input, resets the stored GUID and dismisses the + * overlay. + * @private + */ + dismissOverlay_: function() { + this.clearInputFields_(); + this.guid = ''; + OptionsPage.closeOverlay(); + }, + + /** + * Aggregates the values in the input fields into an array and sends the + * array to the Autofill handler. + * @private + */ + saveAddress_: function() { + var address = new Array(); + address[0] = this.guid; + var list = $('full-name-list'); + address[1] = list.dataModel.slice(0, list.dataModel.length - 1); + address[2] = $('company-name').value; + address[3] = $('addr-line-1').value; + address[4] = $('addr-line-2').value; + address[5] = $('city').value; + address[6] = $('state').value; + address[7] = $('postal-code').value; + address[8] = $('country').value; + list = $('phone-list'); + address[9] = list.dataModel.slice(0, list.dataModel.length - 1); + list = $('email-list'); + address[10] = list.dataModel.slice(0, list.dataModel.length - 1); + + chrome.send('setAddress', address); + }, + + /** + * Connects each input field to the inputFieldChanged_() method that enables + * or disables the 'Ok' button based on whether all the fields are empty or + * not. + * @private + */ + connectInputEvents_: function() { + var self = this; + $('company-name').oninput = $('addr-line-1').oninput = + $('addr-line-2').oninput = $('city').oninput = $('state').oninput = + $('postal-code').oninput = function(event) { + self.inputFieldChanged_(); + }; + + $('country').onchange = function(event) { + self.countryChanged_(); + }; + }, + + /** + * Checks the values of each of the input fields and disables the 'Ok' + * button if all of the fields are empty. + * @private + */ + inputFieldChanged_: function() { + // Length of lists are tested for <= 1 due to the "add" placeholder item + // in the list. + var disabled = + $('full-name-list').items.length <= 1 && + !$('company-name').value && + !$('addr-line-1').value && !$('addr-line-2').value && + !$('city').value && !$('state').value && !$('postal-code').value && + !$('country').value && $('phone-list').items.length <= 1 && + $('email-list').items.length <= 1; + $('autofill-edit-address-apply-button').disabled = disabled; + }, + + /** + * Updates the postal code and state field labels appropriately for the + * selected country. + * @private + */ + countryChanged_: function() { + var countryCode = $('country').value || + loadTimeData.getString('defaultCountryCode'); + + var details = loadTimeData.getValue('autofillCountryData')[countryCode]; + var postal = $('postal-code-label'); + postal.textContent = details.postalCodeLabel; + $('state-label').textContent = details.stateLabel; + + // Also update the 'Ok' button as needed. + this.inputFieldChanged_(); + }, + + /** + * Populates the country <select> list. + * @private + */ + populateCountryList_: function() { + var countryList = loadTimeData.getValue('autofillCountrySelectList'); + + // Add the countries to the country <select> list. + var countrySelect = $('country'); + // Add an empty option. + countrySelect.appendChild(new Option('', '')); + for (var i = 0; i < countryList.length; i++) { + var option = new Option(countryList[i].name, + countryList[i].value); + option.disabled = countryList[i].value == 'separator'; + countrySelect.appendChild(option); + } + }, + + /** + * Clears the value of each input field. + * @private + */ + clearInputFields_: function() { + this.setMultiValueList_('full-name-list', []); + $('company-name').value = ''; + $('addr-line-1').value = ''; + $('addr-line-2').value = ''; + $('city').value = ''; + $('state').value = ''; + $('postal-code').value = ''; + $('country').value = ''; + this.setMultiValueList_('phone-list', []); + this.setMultiValueList_('email-list', []); + + this.countryChanged_(); + }, + + /** + * Loads the address data from |address|, sets the input fields based on + * this data and stores the GUID of the address. + * @private + */ + loadAddress_: function(address) { + this.setInputFields_(address); + this.inputFieldChanged_(); + this.guid = address.guid; + }, + + /** + * Sets the value of each input field according to |address| + * @private + */ + setInputFields_: function(address) { + this.setMultiValueList_('full-name-list', address.fullName); + $('company-name').value = address.companyName; + $('addr-line-1').value = address.addrLine1; + $('addr-line-2').value = address.addrLine2; + $('city').value = address.city; + $('state').value = address.state; + $('postal-code').value = address.postalCode; + $('country').value = address.country; + this.setMultiValueList_('phone-list', address.phone); + this.setMultiValueList_('email-list', address.email); + + this.countryChanged_(); + }, + }; + + AutofillEditAddressOverlay.loadAddress = function(address) { + AutofillEditAddressOverlay.getInstance().loadAddress_(address); + }; + + AutofillEditAddressOverlay.setTitle = function(title) { + $('autofill-address-title').textContent = title; + }; + + AutofillEditAddressOverlay.setValidatedPhoneNumbers = function(numbers) { + AutofillEditAddressOverlay.getInstance().setMultiValueList_('phone-list', + numbers); + }; + + // Export + return { + AutofillEditAddressOverlay: AutofillEditAddressOverlay + }; +}); diff --git a/chromium/chrome/browser/resources/options/autofill_edit_creditcard_overlay.html b/chromium/chrome/browser/resources/options/autofill_edit_creditcard_overlay.html new file mode 100644 index 00000000000..3c5b07f5a57 --- /dev/null +++ b/chromium/chrome/browser/resources/options/autofill_edit_creditcard_overlay.html @@ -0,0 +1,31 @@ +<div id="autofill-edit-credit-card-overlay" class="page" hidden> + <div class="close-button"></div> + <h1 id="autofill-credit-card-title"></h1> + <div class="content-area"> + <label class="settings-row"> + <div i18n-content="nameOnCardLabel"></div> + <input id="name-on-card" type="text"> + </label> + + <label class="settings-row"> + <div i18n-content="creditCardNumberLabel"></div> + <input id="credit-card-number" type="text"> + </label> + + <div class="settings-row"> + <div id="creditCardExpirationLabel" + i18n-content="creditCardExpirationDateLabel"></div> + <select id="expiration-month" aria-labelledby="creditCardExpirationLabel"> + </select> + <select id="expiration-year" aria-labelledby="creditCardExpirationLabel"> + </select> + </div> + </div> + + <div class="action-area button-strip"> + <button id="autofill-edit-credit-card-cancel-button" type="reset" + i18n-content="cancel"></button> + <button id="autofill-edit-credit-card-apply-button" type="submit" + class="default-button" i18n-content="ok" disabled></button> + </div> +</div> diff --git a/chromium/chrome/browser/resources/options/autofill_edit_creditcard_overlay.js b/chromium/chrome/browser/resources/options/autofill_edit_creditcard_overlay.js new file mode 100644 index 00000000000..d836a1539b7 --- /dev/null +++ b/chromium/chrome/browser/resources/options/autofill_edit_creditcard_overlay.js @@ -0,0 +1,206 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + /** @const */ var OptionsPage = options.OptionsPage; + + /** + * AutofillEditCreditCardOverlay class + * Encapsulated handling of the 'Add Page' overlay page. + * @class + */ + function AutofillEditCreditCardOverlay() { + OptionsPage.call(this, 'autofillEditCreditCard', + loadTimeData.getString('autofillEditCreditCardTitle'), + 'autofill-edit-credit-card-overlay'); + } + + cr.addSingletonGetter(AutofillEditCreditCardOverlay); + + AutofillEditCreditCardOverlay.prototype = { + __proto__: OptionsPage.prototype, + + /** + * Initializes the page. + */ + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + + var self = this; + $('autofill-edit-credit-card-cancel-button').onclick = function(event) { + self.dismissOverlay_(); + }; + $('autofill-edit-credit-card-apply-button').onclick = function(event) { + self.saveCreditCard_(); + self.dismissOverlay_(); + }; + + self.guid_ = ''; + self.clearInputFields_(); + self.connectInputEvents_(); + self.setDefaultSelectOptions_(); + }, + + /** + * Specifically catch the situations in which the overlay is cancelled + * externally (e.g. by pressing <Esc>), so that the input fields and + * GUID can be properly cleared. + * @override + */ + handleCancel: function() { + this.dismissOverlay_(); + }, + + /** + * Clears any uncommitted input, and dismisses the overlay. + * @private + */ + dismissOverlay_: function() { + this.clearInputFields_(); + this.guid_ = ''; + OptionsPage.closeOverlay(); + }, + + /** + * Aggregates the values in the input fields into an array and sends the + * array to the Autofill handler. + * @private + */ + saveCreditCard_: function() { + var creditCard = new Array(5); + creditCard[0] = this.guid_; + creditCard[1] = $('name-on-card').value; + creditCard[2] = $('credit-card-number').value; + creditCard[3] = $('expiration-month').value; + creditCard[4] = $('expiration-year').value; + chrome.send('setCreditCard', creditCard); + }, + + /** + * Connects each input field to the inputFieldChanged_() method that enables + * or disables the 'Ok' button based on whether all the fields are empty or + * not. + * @private + */ + connectInputEvents_: function() { + var ccNumber = $('credit-card-number'); + $('name-on-card').oninput = ccNumber.oninput = + $('expiration-month').onchange = $('expiration-year').onchange = + this.inputFieldChanged_.bind(this); + }, + + /** + * Checks the values of each of the input fields and disables the 'Ok' + * button if all of the fields are empty. + * @param {Event} opt_event Optional data for the 'input' event. + * @private + */ + inputFieldChanged_: function(opt_event) { + var disabled = !$('name-on-card').value && !$('credit-card-number').value; + $('autofill-edit-credit-card-apply-button').disabled = disabled; + }, + + /** + * Sets the default values of the options in the 'Expiration date' select + * controls. + * @private + */ + setDefaultSelectOptions_: function() { + // Set the 'Expiration month' default options. + var expirationMonth = $('expiration-month'); + expirationMonth.options.length = 0; + for (var i = 1; i <= 12; ++i) { + var text; + if (i < 10) + text = '0' + i; + else + text = i; + + var option = document.createElement('option'); + option.text = text; + option.value = text; + expirationMonth.add(option, null); + } + + // Set the 'Expiration year' default options. + var expirationYear = $('expiration-year'); + expirationYear.options.length = 0; + + var date = new Date(); + var year = parseInt(date.getFullYear()); + for (var i = 0; i < 10; ++i) { + var text = year + i; + var option = document.createElement('option'); + option.text = text; + option.value = text; + expirationYear.add(option, null); + } + }, + + /** + * Clears the value of each input field. + * @private + */ + clearInputFields_: function() { + $('name-on-card').value = ''; + $('credit-card-number').value = ''; + $('expiration-month').selectedIndex = 0; + $('expiration-year').selectedIndex = 0; + + // Reset the enabled status of the 'Ok' button. + this.inputFieldChanged_(); + }, + + /** + * Sets the value of each input field according to |creditCard| + * @private + */ + setInputFields_: function(creditCard) { + $('name-on-card').value = creditCard.nameOnCard; + $('credit-card-number').value = creditCard.creditCardNumber; + + // The options for the year select control may be out-dated at this point, + // e.g. the user opened the options page before midnight on New Year's Eve + // and then loaded a credit card profile to edit in the new year, so + // reload the select options just to be safe. + this.setDefaultSelectOptions_(); + + var idx = parseInt(creditCard.expirationMonth, 10); + $('expiration-month').selectedIndex = idx - 1; + + expYear = creditCard.expirationYear; + var date = new Date(); + var year = parseInt(date.getFullYear()); + for (var i = 0; i < 10; ++i) { + var text = year + i; + if (expYear == String(text)) + $('expiration-year').selectedIndex = i; + } + }, + + /** + * Loads the credit card data from |creditCard|, sets the input fields based + * on this data and stores the GUID of the credit card. + * @private + */ + loadCreditCard_: function(creditCard) { + this.setInputFields_(creditCard); + this.inputFieldChanged_(); + this.guid_ = creditCard.guid; + }, + }; + + AutofillEditCreditCardOverlay.loadCreditCard = function(creditCard) { + AutofillEditCreditCardOverlay.getInstance().loadCreditCard_(creditCard); + }; + + AutofillEditCreditCardOverlay.setTitle = function(title) { + $('autofill-credit-card-title').textContent = title; + }; + + // Export + return { + AutofillEditCreditCardOverlay: AutofillEditCreditCardOverlay + }; +}); diff --git a/chromium/chrome/browser/resources/options/autofill_edit_overlay.css b/chromium/chrome/browser/resources/options/autofill_edit_overlay.css new file mode 100644 index 00000000000..166f0cd3da1 --- /dev/null +++ b/chromium/chrome/browser/resources/options/autofill_edit_overlay.css @@ -0,0 +1,84 @@ +/* Copyright (c) 2012 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ + +#autofill-edit-address-overlay { + min-width: 55em; +} + +#autofill-edit-credit-card-overlay { + min-width: 500px; +} + +#full-name-list input, +#company-name, +#addr-line-1, +#addr-line-2 { + width: 16em; +} + +#country { + max-width: 450px; +} + +#autofill-edit-address-overlay list { + -webkit-margin-start: -3px; + /* Min height is a multiple of the list item height (32px). */ + min-height: 32px; +} + +#autofill-edit-address-overlay list div.static-text { + -webkit-border-radius: 2px; + -webkit-box-flex: 1; + -webkit-padding-end: 4px; + -webkit-padding-start: 4px; + border: 1px solid darkGray; + /* Set the line-height and min-height to match the height of an input element, + * so that even empty cells renderer with the correct height. */ + line-height: 1.75em; + min-height: 1.75em; +} + +:-webkit-any(#autofill-edit-credit-card-overlay, #autofill-edit-address-overlay) + .settings-row div + :-webkit-any(input, select) { + margin-top: 4px; +} + +#autofill-name-labels { + display: -webkit-inline-box; +} + +#autofill-name-labels span { + -webkit-box-flex: 1; + display: block; +} + +#full-name-list { + display: inline-block; +} + +#full-name-list div[role='listitem'] > div { + display: -webkit-box; +} + +#full-name-list div[role='listitem'] > div > div, +#autofill-name-labels span { + -webkit-margin-end: 5px; + width: 16em; +} + +:-webkit-any(#phone-list, #email-list) div[role='listitem'] > div > div, +:-webkit-any(#phone-list, #email-list) input { + width: 14em; +} + +.input-group > * { + -webkit-box-orient: vertical; + -webkit-margin-end: 2px; + display: -webkit-inline-box; + vertical-align: top; +} + +#autofill-edit-credit-card-overlay .content-area > *:first-child { + margin-top: 0; +} diff --git a/chromium/chrome/browser/resources/options/autofill_options.css b/chromium/chrome/browser/resources/options/autofill_options.css new file mode 100644 index 00000000000..e777c941248 --- /dev/null +++ b/chromium/chrome/browser/resources/options/autofill_options.css @@ -0,0 +1,52 @@ +/* Copyright (c) 2012 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ + +#autofill-options { + width: 550px; +} + +#autofill-help { + bottom: 18px; + position: absolute; +} + +#autofill-options list { + min-height: 172px; +} + +.autofill-list-item { + -webkit-box-flex: 1; + -webkit-padding-start: 8px; + overflow: hidden; + text-overflow: ellipsis; +} + +.autofill-list-item + img { + -webkit-padding-end: 20px; + vertical-align: top; +} + +#autofill-options > div:last-child { + margin-top: 15px; +} + +#autofill-options > div.settings-list > div:last-child { + border-top: 1px solid #d9d9d9; + padding: 5px 10px; +} + +#autofill-add-address, +#autofill-add-creditcard { + margin: 5px 5px; +} + +#autofill-options .list-inline-button { + margin-top: 0; + vertical-align: top; +} + +#autofill-options div[role='listitem']:not(:hover):not([selected]) + .list-inline-button { + display: none; +} diff --git a/chromium/chrome/browser/resources/options/autofill_options.html b/chromium/chrome/browser/resources/options/autofill_options.html new file mode 100644 index 00000000000..27a869aced5 --- /dev/null +++ b/chromium/chrome/browser/resources/options/autofill_options.html @@ -0,0 +1,40 @@ +<div id="autofill-options" class="page" hidden> + <div class="close-button"></div> + <h1 i18n-content="autofillOptionsPage"></h1> + <div class="content-area"> +<if expr="is_macosx"> + <div class="checkbox"> + <label> + <input pref="autofill.auxiliary_profiles_enabled" type="checkbox" + metric="Options_AutofillAuxiliaryProfiles"> + <span i18n-content="auxiliaryProfilesEnabled"></span> + </label> + </div> +</if> + <h3 i18n-content="autofillAddresses"></h3> + <div class="settings-list"> + <list id="address-list"></list> + <div> + <button id="autofill-add-address" i18n-content="autofillAddAddress"> + </button> + </div> + </div> + <h3 i18n-content="autofillCreditCards"></h3> + <div class="settings-list"> + <list id="creditcard-list"></list> + <div> + <button id="autofill-add-creditcard" + i18n-content="autofillAddCreditCard"></button> + </div> + </div> + </div> + <div class="action-area"> + <a id="autofill-help" target="_blank" i18n-values="href:helpUrl" + i18n-content="helpButton"> + </a> + <div class="button-strip"> + <button id="autofill-options-confirm" class="default-button" + i18n-content="done"></button> + </div> + </div> +</div> diff --git a/chromium/chrome/browser/resources/options/autofill_options.js b/chromium/chrome/browser/resources/options/autofill_options.js new file mode 100644 index 00000000000..65fb71964ae --- /dev/null +++ b/chromium/chrome/browser/resources/options/autofill_options.js @@ -0,0 +1,216 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + var OptionsPage = options.OptionsPage; + var ArrayDataModel = cr.ui.ArrayDataModel; + + ///////////////////////////////////////////////////////////////////////////// + // AutofillOptions class: + + /** + * Encapsulated handling of Autofill options page. + * @constructor + */ + function AutofillOptions() { + OptionsPage.call(this, + 'autofill', + loadTimeData.getString('autofillOptionsPageTabTitle'), + 'autofill-options'); + } + + cr.addSingletonGetter(AutofillOptions); + + AutofillOptions.prototype = { + __proto__: OptionsPage.prototype, + + /** + * The address list. + * @type {DeletableItemList} + * @private + */ + addressList_: null, + + /** + * The credit card list. + * @type {DeletableItemList} + * @private + */ + creditCardList_: null, + + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + + this.createAddressList_(); + this.createCreditCardList_(); + + var self = this; + $('autofill-add-address').onclick = function(event) { + self.showAddAddressOverlay_(); + }; + $('autofill-add-creditcard').onclick = function(event) { + self.showAddCreditCardOverlay_(); + }; + $('autofill-options-confirm').onclick = function(event) { + OptionsPage.closeOverlay(); + }; + + // TODO(jhawkins): What happens when Autofill is disabled whilst on the + // Autofill options page? + }, + + /** + * Creates, decorates and initializes the address list. + * @private + */ + createAddressList_: function() { + this.addressList_ = $('address-list'); + options.autofillOptions.AutofillAddressList.decorate(this.addressList_); + this.addressList_.autoExpands = true; + }, + + /** + * Creates, decorates and initializes the credit card list. + * @private + */ + createCreditCardList_: function() { + this.creditCardList_ = $('creditcard-list'); + options.autofillOptions.AutofillCreditCardList.decorate( + this.creditCardList_); + this.creditCardList_.autoExpands = true; + }, + + /** + * Shows the 'Add address' overlay, specifically by loading the + * 'Edit address' overlay and modifying the overlay title. + * @private + */ + showAddAddressOverlay_: function() { + var title = loadTimeData.getString('addAddressTitle'); + AutofillEditAddressOverlay.setTitle(title); + OptionsPage.navigateToPage('autofillEditAddress'); + }, + + /** + * Shows the 'Add credit card' overlay, specifically by loading the + * 'Edit credit card' overlay and modifying the overlay title. + * @private + */ + showAddCreditCardOverlay_: function() { + var title = loadTimeData.getString('addCreditCardTitle'); + AutofillEditCreditCardOverlay.setTitle(title); + OptionsPage.navigateToPage('autofillEditCreditCard'); + }, + + /** + * Updates the data model for the address list with the values from + * |entries|. + * @param {Array} entries The list of addresses. + */ + setAddressList_: function(entries) { + this.addressList_.dataModel = new ArrayDataModel(entries); + }, + + /** + * Updates the data model for the credit card list with the values from + * |entries|. + * @param {Array} entries The list of credit cards. + */ + setCreditCardList_: function(entries) { + this.creditCardList_.dataModel = new ArrayDataModel(entries); + }, + + /** + * Removes the Autofill address or credit card represented by |guid|. + * @param {string} guid The GUID of the address to remove. + * @private + */ + removeData_: function(guid) { + chrome.send('removeData', [guid]); + }, + + /** + * Requests profile data for the address represented by |guid| from the + * PersonalDataManager. Once the data is loaded, the AutofillOptionsHandler + * calls showEditAddressOverlay(). + * @param {string} guid The GUID of the address to edit. + * @private + */ + loadAddressEditor_: function(guid) { + chrome.send('loadAddressEditor', [guid]); + }, + + /** + * Requests profile data for the credit card represented by |guid| from the + * PersonalDataManager. Once the data is loaded, the AutofillOptionsHandler + * calls showEditCreditCardOverlay(). + * @param {string} guid The GUID of the credit card to edit. + * @private + */ + loadCreditCardEditor_: function(guid) { + chrome.send('loadCreditCardEditor', [guid]); + }, + + /** + * Shows the 'Edit address' overlay, using the data in |address| to fill the + * input fields. |address| is a list with one item, an associative array + * that contains the address data. + * @private + */ + showEditAddressOverlay_: function(address) { + var title = loadTimeData.getString('editAddressTitle'); + AutofillEditAddressOverlay.setTitle(title); + AutofillEditAddressOverlay.loadAddress(address); + OptionsPage.navigateToPage('autofillEditAddress'); + }, + + /** + * Shows the 'Edit credit card' overlay, using the data in |credit_card| to + * fill the input fields. |address| is a list with one item, an associative + * array that contains the credit card data. + * @private + */ + showEditCreditCardOverlay_: function(creditCard) { + var title = loadTimeData.getString('editCreditCardTitle'); + AutofillEditCreditCardOverlay.setTitle(title); + AutofillEditCreditCardOverlay.loadCreditCard(creditCard); + OptionsPage.navigateToPage('autofillEditCreditCard'); + }, + }; + + AutofillOptions.setAddressList = function(entries) { + AutofillOptions.getInstance().setAddressList_(entries); + }; + + AutofillOptions.setCreditCardList = function(entries) { + AutofillOptions.getInstance().setCreditCardList_(entries); + }; + + AutofillOptions.removeData = function(guid) { + AutofillOptions.getInstance().removeData_(guid); + }; + + AutofillOptions.loadAddressEditor = function(guid) { + AutofillOptions.getInstance().loadAddressEditor_(guid); + }; + + AutofillOptions.loadCreditCardEditor = function(guid) { + AutofillOptions.getInstance().loadCreditCardEditor_(guid); + }; + + AutofillOptions.editAddress = function(address) { + AutofillOptions.getInstance().showEditAddressOverlay_(address); + }; + + AutofillOptions.editCreditCard = function(creditCard) { + AutofillOptions.getInstance().showEditCreditCardOverlay_(creditCard); + }; + + // Export + return { + AutofillOptions: AutofillOptions + }; + +}); + diff --git a/chromium/chrome/browser/resources/options/autofill_options_list.js b/chromium/chrome/browser/resources/options/autofill_options_list.js new file mode 100644 index 00000000000..0be698d3e2a --- /dev/null +++ b/chromium/chrome/browser/resources/options/autofill_options_list.js @@ -0,0 +1,508 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options.autofillOptions', function() { + /** @const */ var DeletableItem = options.DeletableItem; + /** @const */ var DeletableItemList = options.DeletableItemList; + /** @const */ var InlineEditableItem = options.InlineEditableItem; + /** @const */ var InlineEditableItemList = options.InlineEditableItemList; + + function AutofillEditProfileButton(guid, edit) { + var editButtonEl = document.createElement('button'); + editButtonEl.className = 'list-inline-button custom-appearance'; + editButtonEl.textContent = + loadTimeData.getString('autofillEditProfileButton'); + editButtonEl.onclick = function(e) { edit(guid); }; + + editButtonEl.onmousedown = function(e) { + // Don't select the row when clicking the button. + e.stopPropagation(); + // Don't focus on the button when clicking it. + e.preventDefault(); + }; + + return editButtonEl; + } + + /** + * Creates a new address list item. + * @param {Array} entry An array of the form [guid, label]. + * @constructor + * @extends {options.DeletableItem} + */ + function AddressListItem(entry) { + var el = cr.doc.createElement('div'); + el.guid = entry[0]; + el.label = entry[1]; + el.__proto__ = AddressListItem.prototype; + el.decorate(); + + return el; + } + + AddressListItem.prototype = { + __proto__: DeletableItem.prototype, + + /** @override */ + decorate: function() { + DeletableItem.prototype.decorate.call(this); + + // The stored label. + var label = this.ownerDocument.createElement('div'); + label.className = 'autofill-list-item'; + label.textContent = this.label; + this.contentElement.appendChild(label); + + // The 'Edit' button. + var editButtonEl = new AutofillEditProfileButton( + this.guid, + AutofillOptions.loadAddressEditor); + this.contentElement.appendChild(editButtonEl); + }, + }; + + /** + * Creates a new credit card list item. + * @param {Array} entry An array of the form [guid, label, icon]. + * @constructor + * @extends {options.DeletableItem} + */ + function CreditCardListItem(entry) { + var el = cr.doc.createElement('div'); + el.guid = entry[0]; + el.label = entry[1]; + el.icon = entry[2]; + el.description = entry[3]; + el.__proto__ = CreditCardListItem.prototype; + el.decorate(); + + return el; + } + + CreditCardListItem.prototype = { + __proto__: DeletableItem.prototype, + + /** @override */ + decorate: function() { + DeletableItem.prototype.decorate.call(this); + + // The stored label. + var label = this.ownerDocument.createElement('div'); + label.className = 'autofill-list-item'; + label.textContent = this.label; + this.contentElement.appendChild(label); + + // The credit card icon. + var icon = this.ownerDocument.createElement('img'); + icon.src = this.icon; + icon.alt = this.description; + this.contentElement.appendChild(icon); + + // The 'Edit' button. + var editButtonEl = new AutofillEditProfileButton( + this.guid, + AutofillOptions.loadCreditCardEditor); + this.contentElement.appendChild(editButtonEl); + }, + }; + + /** + * Creates a new value list item. + * @param {AutofillValuesList} list The parent list of this item. + * @param {string} entry A string value. + * @constructor + * @extends {options.InlineEditableItem} + */ + function ValuesListItem(list, entry) { + var el = cr.doc.createElement('div'); + el.list = list; + el.value = entry ? entry : ''; + el.__proto__ = ValuesListItem.prototype; + el.decorate(); + + return el; + } + + ValuesListItem.prototype = { + __proto__: InlineEditableItem.prototype, + + /** @override */ + decorate: function() { + InlineEditableItem.prototype.decorate.call(this); + + // Note: This must be set prior to calling |createEditableTextCell|. + this.isPlaceholder = !this.value; + + // The stored value. + var cell = this.createEditableTextCell(this.value); + this.contentElement.appendChild(cell); + this.input = cell.querySelector('input'); + + if (this.isPlaceholder) { + this.input.placeholder = this.list.getAttribute('placeholder'); + this.deletable = false; + } + + this.addEventListener('commitedit', this.onEditCommitted_); + }, + + /** + * @return {string} This item's value. + * @protected + */ + value_: function() { + return this.input.value; + }, + + /** + * @param {Object} value The value to test. + * @return {boolean} True if the given value is non-empty. + * @protected + */ + valueIsNonEmpty_: function(value) { + return !!value; + }, + + /** + * @return {boolean} True if value1 is logically equal to value2. + */ + valuesAreEqual_: function(value1, value2) { + return value1 === value2; + }, + + /** + * Clears the item's value. + * @protected + */ + clearValue_: function() { + this.input.value = ''; + }, + + /** + * Called when committing an edit. + * If this is an "Add ..." item, committing a non-empty value adds that + * value to the end of the values list, but also leaves this "Add ..." item + * in place. + * @param {Event} e The end event. + * @private + */ + onEditCommitted_: function(e) { + var value = this.value_(); + var i = this.list.items.indexOf(this); + if (i < this.list.dataModel.length && + this.valuesAreEqual_(value, this.list.dataModel.item(i))) { + return; + } + + var entries = this.list.dataModel.slice(); + if (this.valueIsNonEmpty_(value) && + !entries.some(this.valuesAreEqual_.bind(this, value))) { + // Update with new value. + if (this.isPlaceholder) { + // It is important that updateIndex is done before validateAndSave. + // Otherwise we can not be sure about AddRow index. + this.list.dataModel.updateIndex(i); + this.list.validateAndSave(i, 0, value); + } else { + this.list.validateAndSave(i, 1, value); + } + } else { + // Reject empty values and duplicates. + if (!this.isPlaceholder) + this.list.dataModel.splice(i, 1); + else + this.clearValue_(); + } + }, + }; + + /** + * Creates a new name value list item. + * @param {AutofillNameValuesList} list The parent list of this item. + * @param {array} entry An array of [first, middle, last] names. + * @constructor + * @extends {options.ValuesListItem} + */ + function NameListItem(list, entry) { + var el = cr.doc.createElement('div'); + el.list = list; + el.first = entry ? entry[0] : ''; + el.middle = entry ? entry[1] : ''; + el.last = entry ? entry[2] : ''; + el.__proto__ = NameListItem.prototype; + el.decorate(); + + return el; + } + + NameListItem.prototype = { + __proto__: ValuesListItem.prototype, + + /** @override */ + decorate: function() { + InlineEditableItem.prototype.decorate.call(this); + + // Note: This must be set prior to calling |createEditableTextCell|. + this.isPlaceholder = !this.first && !this.middle && !this.last; + + // The stored value. + // For the simulated static "input element" to display correctly, the + // value must not be empty. We use a space to force the UI to render + // correctly when the value is logically empty. + var cell = this.createEditableTextCell(this.first); + this.contentElement.appendChild(cell); + this.firstNameInput = cell.querySelector('input'); + + cell = this.createEditableTextCell(this.middle); + this.contentElement.appendChild(cell); + this.middleNameInput = cell.querySelector('input'); + + cell = this.createEditableTextCell(this.last); + this.contentElement.appendChild(cell); + this.lastNameInput = cell.querySelector('input'); + + if (this.isPlaceholder) { + this.firstNameInput.placeholder = + loadTimeData.getString('autofillAddFirstNamePlaceholder'); + this.middleNameInput.placeholder = + loadTimeData.getString('autofillAddMiddleNamePlaceholder'); + this.lastNameInput.placeholder = + loadTimeData.getString('autofillAddLastNamePlaceholder'); + this.deletable = false; + } + + this.addEventListener('commitedit', this.onEditCommitted_); + }, + + /** @override */ + value_: function() { + return [this.firstNameInput.value, + this.middleNameInput.value, + this.lastNameInput.value]; + }, + + /** @override */ + valueIsNonEmpty_: function(value) { + return value[0] || value[1] || value[2]; + }, + + /** @override */ + valuesAreEqual_: function(value1, value2) { + // First, check for null values. + if (!value1 || !value2) + return value1 == value2; + + return value1[0] === value2[0] && + value1[1] === value2[1] && + value1[2] === value2[2]; + }, + + /** @override */ + clearValue_: function() { + this.firstNameInput.value = ''; + this.middleNameInput.value = ''; + this.lastNameInput.value = ''; + }, + }; + + /** + * Base class for shared implementation between address and credit card lists. + * @constructor + * @extends {options.DeletableItemList} + */ + var AutofillProfileList = cr.ui.define('list'); + + AutofillProfileList.prototype = { + __proto__: DeletableItemList.prototype, + + decorate: function() { + DeletableItemList.prototype.decorate.call(this); + + this.addEventListener('blur', this.onBlur_); + }, + + /** + * When the list loses focus, unselect all items in the list. + * @private + */ + onBlur_: function() { + this.selectionModel.unselectAll(); + }, + }; + + /** + * Create a new address list. + * @constructor + * @extends {options.AutofillProfileList} + */ + var AutofillAddressList = cr.ui.define('list'); + + AutofillAddressList.prototype = { + __proto__: AutofillProfileList.prototype, + + decorate: function() { + AutofillProfileList.prototype.decorate.call(this); + }, + + /** @override */ + activateItemAtIndex: function(index) { + AutofillOptions.loadAddressEditor(this.dataModel.item(index)[0]); + }, + + /** @override */ + createItem: function(entry) { + return new AddressListItem(entry); + }, + + /** @override */ + deleteItemAtIndex: function(index) { + AutofillOptions.removeData(this.dataModel.item(index)[0]); + }, + }; + + /** + * Create a new credit card list. + * @constructor + * @extends {options.DeletableItemList} + */ + var AutofillCreditCardList = cr.ui.define('list'); + + AutofillCreditCardList.prototype = { + __proto__: AutofillProfileList.prototype, + + decorate: function() { + AutofillProfileList.prototype.decorate.call(this); + }, + + /** @override */ + activateItemAtIndex: function(index) { + AutofillOptions.loadCreditCardEditor(this.dataModel.item(index)[0]); + }, + + /** @override */ + createItem: function(entry) { + return new CreditCardListItem(entry); + }, + + /** @override */ + deleteItemAtIndex: function(index) { + AutofillOptions.removeData(this.dataModel.item(index)[0]); + }, + }; + + /** + * Create a new value list. + * @constructor + * @extends {options.InlineEditableItemList} + */ + var AutofillValuesList = cr.ui.define('list'); + + AutofillValuesList.prototype = { + __proto__: InlineEditableItemList.prototype, + + /** @override */ + createItem: function(entry) { + return new ValuesListItem(this, entry); + }, + + /** @override */ + deleteItemAtIndex: function(index) { + this.dataModel.splice(index, 1); + }, + + /** @override */ + shouldFocusPlaceholder: function() { + return false; + }, + + /** + * Called when the list hierarchy as a whole loses or gains focus. + * If the list was focused in response to a mouse click, call into the + * superclass's implementation. If the list was focused in response to a + * keyboard navigation, focus the first item. + * If the list loses focus, unselect all the elements. + * @param {Event} e The change event. + * @private + */ + handleListFocusChange_: function(e) { + // We check to see whether there is a selected item as a proxy for + // distinguishing between mouse- and keyboard-originated focus events. + var selectedItem = this.selectedItem; + if (selectedItem) + InlineEditableItemList.prototype.handleListFocusChange_.call(this, e); + + if (!e.newValue) { + // When the list loses focus, unselect all the elements. + this.selectionModel.unselectAll(); + } else { + // When the list gains focus, select the first item if nothing else is + // selected. + var firstItem = this.getListItemByIndex(0); + if (!selectedItem && firstItem && e.newValue) + firstItem.handleFocus_(); + } + }, + + /** + * Called when a new list item should be validated; subclasses are + * responsible for implementing if validation is required. + * @param {number} index The index of the item that was inserted or changed. + * @param {number} remove The number items to remove. + * @param {string} value The value of the item to insert. + */ + validateAndSave: function(index, remove, value) { + this.dataModel.splice(index, remove, value); + }, + }; + + /** + * Create a new value list for phone number validation. + * @constructor + * @extends {options.AutofillValuesList} + */ + var AutofillNameValuesList = cr.ui.define('list'); + + AutofillNameValuesList.prototype = { + __proto__: AutofillValuesList.prototype, + + /** @override */ + createItem: function(entry) { + return new NameListItem(this, entry); + }, + }; + + /** + * Create a new value list for phone number validation. + * @constructor + * @extends {options.AutofillValuesList} + */ + var AutofillPhoneValuesList = cr.ui.define('list'); + + AutofillPhoneValuesList.prototype = { + __proto__: AutofillValuesList.prototype, + + /** @override */ + validateAndSave: function(index, remove, value) { + var numbers = this.dataModel.slice(0, this.dataModel.length - 1); + numbers.splice(index, remove, value); + var info = new Array(); + info[0] = index; + info[1] = numbers; + info[2] = $('country').value; + chrome.send('validatePhoneNumbers', info); + }, + }; + + return { + AddressListItem: AddressListItem, + CreditCardListItem: CreditCardListItem, + ValuesListItem: ValuesListItem, + NameListItem: NameListItem, + AutofillAddressList: AutofillAddressList, + AutofillCreditCardList: AutofillCreditCardList, + AutofillValuesList: AutofillValuesList, + AutofillNameValuesList: AutofillNameValuesList, + AutofillPhoneValuesList: AutofillPhoneValuesList, + }; +}); diff --git a/chromium/chrome/browser/resources/options/browser_options.css b/chromium/chrome/browser/resources/options/browser_options.css new file mode 100644 index 00000000000..a3dcba14d10 --- /dev/null +++ b/chromium/chrome/browser/resources/options/browser_options.css @@ -0,0 +1,427 @@ +/* Copyright (c) 2012 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ + +#change-home-page-section { + margin-left: 30px; +} + +#home-page-url { + display: inline-block; + max-width: 400px; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: top; +} + +#default-browser-state, +#profiles-supervised-dashboard-tip { + margin-top: 6px; +} + +#sync-overview p { + display: inline; +} + +#account-picture-wrapper { + float: left; + margin: 0 2px 10px 0; +} + +html[dir=rtl] #account-picture-wrapper { + float: right; +} + +#account-picture-control { + border: 1px solid rgba(0, 0, 0, 0.3); + border-radius: 4px; + display: inline-block; + padding: 3px; + position: relative; +} + +#account-picture { + height: 56px; + vertical-align: middle; + width: 56px; +} + +#account-picture:disabled { + cursor: default; +} + +#change-picture-caption { + background: rgba(0, 0, 0, 0.5); + bottom: 0; + color: white; + cursor: pointer; + font-size: small; + margin: 3px 0; + position: absolute; + text-align: center; + visibility: hidden; + /* Width of #account-picture. */ + width: 56px; +} + +#account-picture:not(:disabled):hover + #change-picture-caption, +#account-picture:not(:disabled) + #change-picture-caption:hover { + visibility: visible; +} + +#account-picture-indicator { + -webkit-margin-end: 3px; +} + +#sync-general { + -webkit-margin-start: 76px; + margin-bottom: 10px; +} + +#sync-buttons { + clear: both; +} + +#profiles-list { + margin-bottom: 10px; + min-height: 0; +} + +#profiles-list .profile-name { + -webkit-box-flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +#profiles-list > * { + height: 40px; +} + +#profiles-list:focus { + border-color: rgb(77, 144, 254); +} + +.profile-img { + height: 31px; + padding: 3px; + vertical-align: middle; + width: 38px; +} + +.profile-item-current { + font-weight: bold; +} + +#profiles-buttons { + white-space: nowrap; +} + +.sync-error { + background: rgb(255, 219, 219); + border: 1px solid rgb(206, 76, 76); + border-radius: 2px; + padding: 10px; +} + +.sync-error .link-button { + margin: 0 1ex; + padding: 0; +} + +#mac-passwords-warning { + margin-top: 10px; +} + +input[type='range'] { + vertical-align: middle; +} + +/* CSS tweak to fix crbug.com/151788. Inconsistencies in the CSS rules across + * platforms and elements. Too risky to attempt a general fix for M23 at this + * time. This fix addresses the immediate problem in the bug report by forcing + * the button to align consistently with its neighboring select element. + * TODO(kevers): Revisit padding rules for select and buttons to ensure + * consistency in the size and baseline across all platforms. */ +#manage-default-search-engines { + padding-bottom: 0; + padding-top: 0; +} + +/* Override a platform specific rule in Widgets that may no longer be relevant. + * Too late in the development cycle to update Widgets.css due to the number + * of pages that depend on it. + * TODO(kevers): Rivisit padding rules. */ +#default-search-engine { + padding-bottom: 0; +} + +/* Internet settings */ + +#network-settings { + position: relative; +} + +#network-list { + min-height: 0; + width: 320px; +} + +#download-location-label.disabled { + color: #999; +} + +.network-group { + -webkit-box-orient: horizontal; + height: 42px; + overflow: visible; +} + +list:not([disabled]) > .network-group:hover, +list:not([disabled]) > .network-group[selected] { + background-color: #f8f8f8 !important; + background-image: -webkit-linear-gradient(rgba(255, 255, 255, 0.8), + rgba(255, 255, 255, 0)) !important; + box-shadow: inset 0 0 1px 1px #f0f0f0; +} + +.network-group-labels { + -webkit-box-flex: 1; + -webkit-box-orient: vertical; + display: -webkit-box; + padding-top: 3px; +} + +.network-icon, +.network-menu-item-icon { + -webkit-margin-end: 8px; + background-position: left top; + background-size: 25px; + height: 25px; + width: 25px; +} + +.other-cellulars > .network-menu-item-icon { + background-position: left top; +} + +@-webkit-keyframes connecting-animation { + 0% { + background-position: 0 25%; + } + 12.5% { + background-position: 0 50%; + } + 25% { + background-position: 0 75%; + } + 37.5% { + background-position: 0 100%; + } + 50% { + background-position: 0 100%; + } + 62.5% { + background-position: 0 75%; + } + 75% { + background-position: 0 50%; + } + 87.5% { + background-position: 0 25%; + } +} + +.network-add-connection, +.network-control-active, +.network-control-inactive { + background-position: center center !important; + background-repeat: no-repeat; +} + +.network-add-connection { + background-image: url('chrome://theme/IDR_NETWORK_ADD_CONNECTION'); + background-size: 16px; +} + +.network-control-inactive { + background-image: none; +} + +.network-control-active { + background-image: url('chrome://theme/IDR_PROFILE_SELECTED'); + background-size: 16px; +} + +.network-options-button { + -webkit-box-flex: 0; + -webkit-transform: scale(0.6); + background-image: none; + background-position: center center; + display: block; + opacity: 0.5; + vertical-align: middle; + width: 19px; +} + +.network-group > .controlled-setting-indicator, +.network-menu-item > .controlled-setting-indicator { + -webkit-margin-end: 5px; +} + +.network-options-button:hover { + opacity: 1; +} + +@-webkit-keyframes vpn-connecting-animation { + from { + opacity: 1; + } + to { + opacity: 0.2; + } +} + +.network-connecting { + -webkit-animation: connecting-animation 1s step-end infinite; +} + +.network-vpn.network-connecting { + -webkit-animation: vpn-connecting-animation 500ms alternate infinite; +} + +.network-title { + font-weight: 600; + line-height: 120%; +} + +.network-subtitle { + color: #333; + display: inline-block; + line-height: 100%; + max-width: 260px; + opacity: 0.4; + overflow: hidden; + padding-bottom: 3px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.network-selector { + background: right center no-repeat; + background-image: url('../../../../ui/webui/resources/images/select.png'); + padding-right: 20px; +} + +.network-menu { + -webkit-box-shadow: + 0 0 0 1px rgba(0,0,0,0.1), + 0 5px 1px 1px rgba(0,0,0,0.1), + 0 5px 2px 1px rgba(0,0,0,0.1), + 0 5px 12px 1px rgba(0,0,0,0.5); + background: #fff; + display: block; + position: absolute; + width: 320px; + z-index: 1; +} + +.network-menu-item { + -webkit-box-align: center; + -webkit-box-orient: horizontal; + display: -webkit-box; + height: 32px; + margin-left: 4px; + margin-right: 4px; +} + +.network-menu-item-label { + -webkit-box-flex: 1; + color: #555; + display: block; + overflow-x: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.active-network { + color: black; + font-weight: bold; +} + +.network-disabled-control { + color: #999; +} + +/* Restrict the size of the networks menu, by limiting the number of + visible networks. */ +.network-menu-group { + max-height: 192px; + overflow-x: hidden; + overflow-y: auto; + text-overflow: ellipsis; +} + +.network-menu-item:hover { + background-color: #eee; +} + +.network-menu > hr { + opacity: 0.4; +} + +#shared-proxies { + margin-top: 12px; +} + +#web-content-section select, +.web-content-select-label { + min-width: 145px; +} + +.web-content-select-label > span:only-of-type { + display: inline-block; + min-width: 100px; +} + +#timezone-value { + display: inline-block; + vertical-align: baseline; +} + +#privacy-explanation { + line-height: 1.8em; +} + +#advanced-settings { + height: 0; + margin-top: 8px; + overflow: hidden; +} + +#auto-open-file-types-label { + padding: 0.45em 0 +} + +.sliding { + -webkit-transition: height 200ms; + overflow-y: hidden; +} + +#keyboard-overlay .option-value > select { + width: 100%; +} + +#keyboard-overlay table { + /* Same as .settings-row {margin}. */ + -webkit-border-vertical-spacing: 0.65em; +} + +#accessibility-autoclick .controlled-setting-with-label { + -webkit-box-align: baseline; +} + +#accessibility-autoclick label + select { + /* Same as .controlled-setting-with-label > input + span. */ + -webkit-margin-start: 0.6em; +} diff --git a/chromium/chrome/browser/resources/options/browser_options.html b/chromium/chrome/browser/resources/options/browser_options.html new file mode 100644 index 00000000000..22c61ff490b --- /dev/null +++ b/chromium/chrome/browser/resources/options/browser_options.html @@ -0,0 +1,863 @@ +<div id="settings" class="page" hidden> + <header> + <h1 i18n-content="settingsTitle"></h1> + </header> + <include src="reset_profile_settings_banner.html"> +<if expr="not pp_ifdef('chromeos')"> + <include src="sync_section.html"> +</if> +<if expr="pp_ifdef('chromeos')"> + <section> + <h3 i18n-content="sectionTitleInternet"></h3> + <div id="network-settings"> + <list id="network-list"></list> + <div id="shared-proxies" class="checkbox"> + <label> + <input id="use-shared-proxies" type="checkbox" + pref="settings.use_shared_proxies"> + <span i18n-content="useSharedProxies"></span> + </label> + </div> + <div id="network-menus"></div> + </div> + </section> +</if> +<if expr="not pp_ifdef('chromeos')"> + <include src="startup_section.html"> +</if> + <section> + <h3 i18n-content="sectionTitleAppearance"></h3> + <div class="settings-row"> +<if expr="pp_ifdef('chromeos')"> + <button id="set-wallpaper" i18n-content="setWallpaper" + guest-visibility="disabled"></button> +</if> +<if expr="not pp_ifdef('chromeos') and is_posix and not is_macosx"> + <button id="themes-gallery" i18n-content="themesGallery"></button> + <button id="themes-native-button" + i18n-content="themesNativeButton"></button> + <button id="themes-reset" + i18n-content="themesSetClassic"></button> +</if> +<if expr="pp_ifdef('chromeos') or is_win or is_macosx"> + <button id="themes-gallery" i18n-content="themesGallery"></button> + <button id="themes-reset" i18n-content="themesReset"></button> +</if> + </div> + <div class="checkbox" guest-visibility="disabled"> + <span class="controlled-setting-with-label"> + <input id="show-home-button" type="checkbox" + pref="browser.show_home_button" + metric="Options_Homepage_HomeButton"> + <span> + <label for="show-home-button" i18n-content="homePageShowHomeButton"> + </label> + <span class="controlled-setting-indicator" + pref="browser.show_home_button"> + </span> + </span> + </span> + </div> + <div id="change-home-page-section" hidden> + <div id="change-home-page-section-container" guest-visibility="disabled"> + <span id="home-page-ntp" class="home-page-label" + i18n-content="homePageNtp"></span> + <span id="home-page-url" class="home-page-label"></span> + <button id="change-home-page" class="link-button" + i18n-content="changeHomePage"></button> + </div> + </div> + <div class="checkbox" guest-visibility="disabled"> + <span class="controlled-setting-with-label"> + <input id="show-bookmark-bars" type="checkbox" + pref="bookmark_bar.show_on_all_tabs" + metric="Options_ShowBookmarksBar"> + <span> + <label for="show-bookmark-bars" + i18n-content="toolbarShowBookmarksBar"> + </label> + <span class="controlled-setting-indicator" + pref="bookmark_bar.show_on_all_tabs"> + </span> + </span> + </span> + </div> +<if expr="not pp_ifdef('toolkit_views') and is_posix and not is_macosx"> + <div class="checkbox"><label> + <input id="show-window-decorations" type="checkbox" + pref="browser.custom_chrome_frame" metric="Options_CustomFrame" + inverted_pref> + <span i18n-content="showWindowDecorations"></span> + </label></div> +</if> + </section> +<if expr="pp_ifdef('chromeos')"> + <section> + <h3 i18n-content="sectionTitleDevice"></h3> + <div> + <span i18n-content="deviceGroupDescription"></span> + <div id="touchpad-settings" class="settings-row" hidden> + <span class="option-name" i18n-content="touchpadSpeed"></span> + <input id="sensitivity-range" type="range" min="1" max="5" + pref="settings.touchpad.sensitivity2" class="touch-slider"> + </div> + <div id="mouse-settings" class="settings-row" hidden> + <span class="option-name" i18n-content="mouseSpeed"></span> + <input id="sensitivity-range" type="range" min="1" max="5" + pref="settings.mouse.sensitivity2" class="touch-slider"> + </div> + <div id="no-pointing-devices" i18n-content="noPointingDevices" + class="settings-row" hidden> + </div> + <div class="settings-row"> + <button id="pointer-settings-button" hidden> + </button> + <button id="keyboard-settings-button" + i18n-content="keyboardSettingsButtonTitle"> + </button> + <span id="display-options-section"> + <button id="display-options" i18n-content="displayOptions"> + </button> + </span> + </div> + </div> + </section> +</if> + <section> + <h3 i18n-content="sectionTitleSearch"></h3> + <div> + <label for="default-search-engine" class="settings-row" + i18n-values=".innerHTML:defaultSearchGroupLabel"> + </label> + <div class="settings-row"> + <select id="default-search-engine" class="weakrtl"></select> + <span class="controlled-setting-indicator" + pref="default_search_provider.enabled"> + </span> + <button id="manage-default-search-engines" + i18n-content="defaultSearchManageEngines"> + </button> + </div> + </div> + </section> + <section id="sync-users-section" guest-visibility="hidden"> + <h3 i18n-content="sectionTitleUsers"></h3> +<if expr="pp_ifdef('chromeos')"> + <include src="sync_section.html"> +</if> + <div id="profiles-section" hidden> + <list id="profiles-list" class="settings-list" hidden></list> + <div id="profiles-single-message" class="settings-row" + i18n-content="profilesSingleUser"> + </div> + <div id="profiles-buttons"> + <button id="profiles-create" i18n-content="profilesCreate"></button> +<if expr="pp_ifdef('enable_settings_app')"> + <button id="profiles-app-list-switch" + i18n-content="profilesAppListSwitch" hidden> + </button> +</if> + <button id="profiles-manage" i18n-content="profilesManage" disabled> + </button> + <button id="profiles-delete" i18n-content="profilesDelete"></button> +<if expr="not pp_ifdef('chromeos')"> + <button id="import-data" i18n-content="importData"></button> +</if> + </div> + </div> + <div id="profiles-supervised-dashboard-tip" + i18n-values=".innerHTML:profilesSupervisedDashboardTip" hidden> + </div> + </section> +<if expr="not pp_ifdef('chromeos')"> + <section> + <h3 i18n-content="sectionTitleDefaultBrowser"></h3> + <div> + <button id="set-as-default-browser" + i18n-content="defaultBrowserUseAsDefault" hidden> + </button> + <div id="default-browser-state" i18n-content="defaultBrowserUnknown"> + </div> + <div id="auto-launch-option" class="checkbox" hidden> + <label id="auto-launch-label"> + <input id="auto-launch" type="checkbox"> + <span i18n-content="autoLaunchText"></span> + </label> + </div> + </div> + </section> +</if> <!-- not pp_ifdef('chromeos') --> +<div id="advanced-settings" hidden> +<div id="advanced-settings-container"> +<if expr="pp_ifdef('chromeos')"> + <section> + <h3 i18n-content="datetimeTitle"></h3> + <div class="option-control-table"> + <div guest-visibility="disabled"> + <label for="timezone-select" class="option-name" + i18n-content="timezone"></label> + <div id="timezone-value"> + <select id="timezone-select" class="control" + i18n-options="timezoneList" data-type="string" + pref="cros.system.timezone"> + </select> + </div> + </div> + <div class="checkbox"> + <label> + <input id="use-24hour-clock" pref="settings.clock.use_24hour_clock" + type="checkbox"> + <span i18n-content="use24HourClock"></span> + </label> + </div> + </div> + </section> +</if> + <section id="privacy-section"> + <h3 i18n-content="advancedSectionTitlePrivacy"></h3> + <div> + <div class="settings-row"> + <button id="privacyContentSettingsButton" + i18n-content="privacyContentSettingsButton"></button> + <button id="privacyClearDataButton" + i18n-content="privacyClearDataButton"></button> + </div> + <p id="privacy-explanation" class="settings-row"> + <span i18n-content="improveBrowsingExperience"></span> + <span i18n-content="disableWebServices"></span> + <a target="_blank" i18n-content="learnMore" + i18n-values="href:privacyLearnMoreURL"></a> + </p> + <div class="checkbox"> + <span class="controlled-setting-with-label"> + <input id="alternateErrorPagesEnabled" + pref="alternate_error_pages.enabled" + metric="Options_LinkDoctorCheckbox" type="checkbox"> + <span> + <label for="alternateErrorPagesEnabled" + i18n-content="linkDoctorPref"> + </label> + <span class="controlled-setting-indicator" + pref="alternate_error_pages.enabled"> + </span> + </span> + </span> + </div> + <div class="checkbox" guest-visibility="disabled"> + <span class="controlled-setting-with-label"> + <input id="searchSuggestEnabled" pref="search.suggest_enabled" + metric="Options_UseSuggestCheckbox" type="checkbox"> + <span> + <label for="searchSuggestEnabled" i18n-content="suggestPref"> + </label> + <span class="controlled-setting-indicator" + pref="search.suggest_enabled"> + </span> + </span> + </span> + </div> + <div class="checkbox" guest-visibility="disabled"> + <span class="controlled-setting-with-label"> + <input id="dnsPrefetchingEnabled" pref="dns_prefetching.enabled" + metric="Options_DnsPrefetchCheckbox" type="checkbox"> + <span> + <label for="dnsPrefetchingEnabled" + i18n-content="networkPredictionEnabledDescription"> + </label> + <span class="controlled-setting-indicator" + pref="dns_prefetching.enabled"> + </span> + </span> + </span> + </div> + <div class="checkbox"> + <span class="controlled-setting-with-label"> + <input id="safeBrowsingEnabled" pref="safebrowsing.enabled" + metric="Options_SafeBrowsingCheckbox" type="checkbox"> + <span> + <label for="safeBrowsingEnabled" + i18n-content="safeBrowsingEnableProtection"> + </label> + <span class="controlled-setting-indicator" + pref="safebrowsing.enabled"> + </span> + </span> + </span> + </div> +<if expr="pp_ifdef('_google_chrome')"> + <div class="checkbox"> + <span class="controlled-setting-with-label"> + <input id="spelling-enabled-control" type="checkbox" + pref="spellcheck.use_spelling_service" dialog-pref> + <span> + <label for="spelling-enabled-control" i18n-content="spellingPref"> + </label> + <span id="spelling-enabled-indicator" + class="controlled-setting-indicator" + pref="spellcheck.use_spelling_service" dialog-pref> + </span> + </span> + </span> + </div> + <div id="metricsReportingSetting" class="checkbox"> + <span class="controlled-setting-with-label"> +<if expr="pp_ifdef('chromeos')"> + <input id="metricsReportingEnabled" + pref="cros.metrics.reportingEnabled" type="checkbox"> + <span> + <label for="metricsReportingEnabled" i18n-content="enableLogging"> + </label> + <span class="controlled-setting-indicator" + pref="cros.metrics.reportingEnabled"> + </span> + </span> +</if> +<if expr="not pp_ifdef('chromeos')"> + <input id="metricsReportingEnabled" + pref="user_experience_metrics.reporting_enabled" type="checkbox"> + <span> + <label for="metricsReportingEnabled" i18n-content="enableLogging"> + </label> + <span class="controlled-setting-indicator" + pref="user_experience_metrics.reporting_enabled"> + </span> + </span> +</if> + </span> + </div> +</if> <!-- pp_ifdef('_google_chrome') --> + <div class="checkbox"> + <label> + <input id="do-not-track-enabled" pref="enable_do_not_track" + metric="Options_DoNotTrackCheckbox" type="checkbox" dialog-pref> + <span i18n-content="doNotTrack"></span> + </label> + </div> +<if expr="pp_ifdef('chromeos')"> + <div class="checkbox"> + <span class="controlled-setting-with-label"> + <input id="content-protection-attestation-enabled" type="checkbox" + pref="cros.device.attestation_for_content_protection_enabled"> + <span> + <label for="content-protection-attestation-enabled" + i18n-content="enableContentProtectionAttestation"> + </label> + <a target="_blank" i18n-content="learnMore" + i18n-values="href:contentProtectionAttestationLearnMoreURL"> + </a> + <span class="controlled-setting-indicator" + pref="cros.device.attestation_for_content_protection_enabled"> + </span> + </span> + </span> + </div> +</if> + </div> + </section> +<if expr="pp_ifdef('chromeos')"> + <!-- By default, the bluetooth section is hidden. It is only visible if a + bluetooth adapter is discovered --> + <section id="bluetooth-devices" hidden> + <h3 i18n-content="bluetooth"></h3> + <div id="bluetooth-options-div"> + <div class="checkbox"> + <label> + <input type="checkbox" id="enable-bluetooth"> + <span i18n-content="enableBluetooth"> + </label> + </div> + <div class="settings-list bluetooth-device-list" hidden> + <list id="bluetooth-paired-devices-list"></list> + <div id="bluetooth-paired-devices-list-empty-placeholder" + class="bluetooth-empty-list-label" hidden> + <span i18n-content="bluetoothNoDevices"></span> + </div> + </div> + <div id="bluetooth-button-group"> + <button id="bluetooth-add-device" + i18n-content="addBluetoothDevice" hidden></button> + <button id="bluetooth-reconnect-device" + i18n-content="bluetoothConnectDevice" disabled hidden></button> + </div> + </div> + </section> +</if> <!-- pp_ifdef('chromeos') --> + <section id="passwords-and-autofill-section"> + <h3 i18n-content="passwordsAndAutofillGroupName"></h3> + <div class="checkbox"> + <span class="controlled-setting-with-label"> + <input id="autofill-enabled" pref="autofill.enabled" + metric="Options_FormAutofill" type="checkbox"> + <span> + <label for="autofill-enabled" i18n-content="autofillEnabled"></label> + <span class="controlled-setting-indicator" pref="autofill.enabled"> + </span> + <button id="autofill-settings" class="link-button" + i18n-content="manageAutofillSettings"> + </button> + </span> + </span> + </div> + <div class="checkbox"> + <span class="controlled-setting-with-label"> + <input id="password-manager-enabled" + pref="profile.password_manager_enabled" + metric="Options_PasswordManager" type="checkbox"> + <span> + <label for="password-manager-enabled" + i18n-content="passwordManagerEnabled"> + </label> + <span class="controlled-setting-indicator" + pref="profile.password_manager_enabled"> + </span> + <button id="manage-passwords" class="link-button" + i18n-content="managePasswords"> + </button> + </span> + </span> + </div> + <div class="checkbox" id="password-generation-checkbox"> + <label> + <input id="password-generation-enabled" pref="password_generation.enabled" + metric="Options_PasswordGenerationCheckbox" type="checkbox"> + <span i18n-content="passwordGenerationEnabledDescription"></span> + </label> + </div> +<if expr="is_macosx"> + <div id="mac-passwords-warning" i18n-content="macPasswordsWarning" hidden> + </div> +</if> + </section> + <section id="web-content-section"> + <h3 i18n-content="advancedSectionTitleContent"></h3> + <div> + <div class="settings-row"> + <label class="web-content-select-label"> + <span i18n-content="defaultFontSizeLabel"></span> + <select id="defaultFontSize"> + <option value="9" i18n-content="fontSizeLabelVerySmall"> + </option> + <option value="12" i18n-content="fontSizeLabelSmall"></option> + <option value="16" i18n-content="fontSizeLabelMedium"></option> + <option value="20" i18n-content="fontSizeLabelLarge"></option> + <option value="24" i18n-content="fontSizeLabelVeryLarge"> + </option> + </select> + </label> + <span id="font-size-indicator" + class="controlled-setting-indicator"></span> + <button id="fontSettingsCustomizeFontsButton" + i18n-content="fontSettingsCustomizeFontsButton"></button> + </div> + <div class="settings-row"> + <label class="web-content-select-label"> + <span i18n-content="defaultZoomFactorLabel"></span> + <select id="defaultZoomFactor" dataType="double"></select> + </label> + </div> +<if expr="is_macosx"> + <div class="checkbox"> + <label> + <input id="tabsToLinksPref" pref="webkit.webprefs.tabs_to_links" + metric="Options_TabsToLinks" type="checkbox"> + <span i18n-content="tabsToLinksPref"></span> + </label> + </div> +</if> + </div> + </section> +<if expr="not pp_ifdef('chromeos')"> + <section id="network-section"> + <h3 i18n-content="advancedSectionTitleNetwork"></h3> + <div> + <span id="proxiesLabel" class="settings-row"></span> + <div class="settings-row"> + <button id="proxiesConfigureButton" + i18n-content="proxiesConfigureButton"></button> + <span class="controlled-setting-indicator" pref="proxy" plural></span> + </div> + </div> + </section> +</if> + <section id="languages-section"> + <h3 i18n-content="advancedSectionTitleLanguages"></h3> + <span class="settings-row" i18n-content="languageSectionLabel"></span> + <div class="settings-row"> + <button id="language-button" + i18n-content="languageAndSpellCheckSettingsButton"></button> + </div> + <div class="checkbox"> + <span class="controlled-setting-with-label"> + <input id="enableTranslate" pref="translate.enabled" + metric="Options_Translate" type="checkbox"> + <span> + <label for="enableTranslate" i18n-content="translateEnableTranslate"> + </label> + <span class="controlled-setting-indicator" pref="translate.enabled"> + </span> + <button id="manage-languages" class="link-button" + i18n-content="manageLanguages"></button> + </span> + </span> + </div> + </section> + <section id="downloads-section"> + <h3 i18n-content="downloadLocationGroupName"></h3> + <div> + <div class="settings-row"> + <label> + <span id="download-location-label" + i18n-content="downloadLocationBrowseTitle"> + </span> + <input id="downloadLocationPath" class="weakrtl" type="text" + size="36" readonly> + </label> + <button id="downloadLocationChangeButton" + i18n-content="downloadLocationChangeButton"> + </button> + <span class="controlled-setting-indicator" + pref="download.default_directory"> + </span> + </div> + <div class="checkbox"> + <span class="controlled-setting-with-label"> + <input id="prompt-for-download" type="checkbox" + pref="download.prompt_for_download" + metric="Options_AskForSaveLocation"> + <span> + <label for="prompt-for-download" + i18n-content="downloadLocationAskForSaveLocation"> + </label> + <span class="controlled-setting-indicator" + pref="download.prompt_for_download"> + </span> + </span> + </span> + </div> +<if expr="pp_ifdef('chromeos')"> + <div class="checkbox" id="disable-drive-row" guest-visibility="disabled"> + <span class="controlled-setting-with-label"> + <input id="drive-disabled" type="checkbox" + pref="gdata.disabled" + metric="Options_DisableGData"> + <span> + <label for="drive-disabled" i18n-content="disableGData"></label> + <span class="controlled-setting-indicator" pref="gdata.disabled"> + </span> + </span> + </span> + </div> +</if> +<if expr="not pp_ifdef('chromeos')"> + <div id="auto-open-file-types-section" hidden> + <div id="auto-open-file-types-container"> + <div id="auto-open-file-types-label" + i18n-content="autoOpenFileTypesInfo"></div> + <button id="autoOpenFileTypesResetToDefault" + i18n-content="autoOpenFileTypesResetToDefault"></button> + </div> + </div> +</if> + </div> + </section> + <section> + <h3 i18n-content="advancedSectionTitleSecurity"></h3> + <div> +<if expr="pp_ifdef('use_nss') or is_win or is_macosx"> + <div class="settings-row"> + <button id="certificatesManageButton" + i18n-content="certificatesManageButton"></button> + </div> +</if> + <div class="checkbox"> + <label> + <input id="sslCheckRevocation" pref="ssl.rev_checking.enabled" + type="checkbox"> + <span i18n-content="sslCheckRevocation"></span> + </label> + </div> + </div> + </section> + <section id="cloud-print-connector-section"> + <h3 i18n-content="advancedSectionTitleCloudPrint"></h3> +<if expr="pp_ifdef('enable_mdns')"> + <div id="cloudprint-options-mdns" hidden> + <div class="settings-row"> + <span i18n-content="cloudPrintOptionLabel"></span> + <a target="_blank" i18n-content="learnMore" + i18n-values="href:cloudPrintLearnMoreURL"></a> + </div> + <button id="cloudPrintDevicesPageButton" + i18n-content="cloudPrintDevicesPageButton"> + </button> + + <div class="checkbox" + i18n-values=".hidden: cloudPrintHideNotificationsCheckbox"> + <span class="controlled-setting-with-label"> + <input id="local-discovery-notifications-enabled" + pref="local_discovery.notifications_enabled" + type="checkbox" + metric="LocalDiscoveryNotificationsDisabled_Settings" /> + <span> + <label for="local-discovery-notifications-enabled" + i18n-content="cloudPrintEnableNotificationsLabel"> + </label> + <span class="controlled-setting-indicator" + pref="local_discovery.notifications_enabled"> + </span> + </span> + </div> + </div> +</if> + + <div id="cloudprint-options-nomdns"> +<if expr="pp_ifdef('chromeos')"> + <div> + <span i18n-content="cloudPrintOptionLabel"></span> + <a target="_blank" i18n-content="learnMore" + i18n-values="href:cloudPrintLearnMoreURL"></a> + </div> +</if> + +<if expr="not pp_ifdef('chromeos')"> + <p id="cloudPrintConnectorLabel" class="settings-row" + i18n-content="cloudPrintConnectorDisabledLabel"></p> +</if> + + <div class="settings-row"> +<if expr="not pp_ifdef('chromeos')"> + <button id="cloudPrintConnectorSetupButton" + i18n-content="cloudPrintConnectorDisabledButton"></button> +</if> + + <button id="cloudPrintManageButton" + i18n-content="cloudPrintManageButton"> + </button> + </div> + </div> + </section> + +<if expr="pp_ifdef('chromeos')"> + <include src="startup_section.html"> + <section> + <h3 i18n-content="accessibilityTitle"></h3> + <div class="option-control-table"> + <p id="accessibility-explanation" class="settings-row"> + <span i18n-content="accessibilityExplanation"></span> + <a target="_blank" i18n-content="learnMore" + i18n-values="href:accessibilityLearnMoreURL"></a> + </p> + <div class="option-name"> + <div class="checkbox"> + <span class="controlled-setting-with-label"> + <input id="accessibility-should-always-show-menu" + pref="settings.a11y.enable_menu" type="checkbox"> + <span> + <label for="accessibility-should-always-show-menu" + i18n-content="accessibilityAlwaysShowMenu"> + </label> + <span class="controlled-setting-indicator" + pref="settings.a11y.enable_menu"> + </span> + </span> + </div> + </div> + <div class="option-name"> + <div class="checkbox"> + <span class="controlled-setting-with-label"> + <input id="accessibility-large-cursor-check" + pref="settings.a11y.large_cursor_enabled" type="checkbox"> + <span> + <label for="accessibility-large-cursor-check" + i18n-content="accessibilityLargeCursor"> + </label> + <span class="controlled-setting-indicator" + pref="settings.a11y.large_cursor_enabled"> + </span> + </span> + </span> + </div> + </div> + <div class="option-name"> + <div class="checkbox"> + <span class="controlled-setting-with-label"> + <input id="accessibility-high-contrast-check" + pref="settings.a11y.high_contrast_enabled" type="checkbox"> + <span> + <label for="accessibility-high-contrast-check" + i18n-content="accessibilityHighContrast"> + </label> + <span class="controlled-setting-indicator" + pref="settings.a11y.high_contrast_enabled"> + </span> + </span> + </span> + </div> + </div> + <div id="accessibility-sticky-keys" class="option-name" hidden> + <div class="checkbox"> + <span class="controlled-setting-with-label"> + <input id="accessibility-sticky-keys-check" + pref="settings.a11y.sticky_keys_enabled" type="checkbox"> + <span> + <label for="accessibility-sticky-keys-check" + i18n-content="accessibilityStickyKeys"> + </label> + <span class="controlled-setting-indicator" + pref="settings.a11y.sticky_keys_enabled"> + </span> + </span> + </span> + </div> + </div> + <div class="option-name"> + <div class="checkbox"> + <span class="controlled-setting-with-label"> + <input id="accessibility-spoken-feedback-check" + pref="settings.accessibility" type="checkbox"> + <span> + <label for="accessibility-spoken-feedback-check" + i18n-content="accessibilitySpokenFeedback"> + </label> + <span class="controlled-setting-indicator" + pref="settings.accessibility"> + </span> + </span> + </span> + <div id="accessibility-settings" hidden> + <button id="accessibility-settings-button" + i18n-content="accessibilitySettings"></button> + </div> + </div> + </div> + <div class="option-name"> + <div class="checkbox"> + <span class="controlled-setting-with-label"> + <input id="accessibility-screen-magnifier-check" + pref="settings.a11y.screen_magnifier" type="checkbox"> + <span> + <label for="accessibility-screen-magnifier-check" + i18n-content="accessibilityScreenMagnifier"> + </label> + <span class="controlled-setting-indicator" + pref="settings.a11y.screen_magnifier"> + </span> + </span> + </span> + </div> + </div> + <div class="option-name" id="accessibility-tap-dragging"> + <div class="checkbox"> + <label> + <input id="accessibility-tap-dragging-check" + pref="settings.touchpad.enable_tap_dragging" type="checkbox"> + <span i18n-content="accessibilityTapDragging"></span> + </label> + </div> + </div> + <div class="option-name" id="accessibility-autoclick"> + <div class="checkbox"> + <span class="controlled-setting-with-label"> + <input id="accessibility-autoclick-check" + pref="settings.a11y.autoclick" type="checkbox"> + <span> + <div> + <div> + <label for="accessibility-autoclick-check" + i18n-content="accessibilityAutoclick"> + </label> + <span class="controlled-setting-indicator" + pref="settings.a11y.autoclick"> + </span> + </div> + <div> + <label for="accessibility-autoclick-dropdown" + i18n-content="accessibilityAutoclickDropdown"> + </label> + <select id="accessibility-autoclick-dropdown" class="control" + data-type="number" + pref="settings.a11y.autoclick_delay_ms"> + <option value="200" + i18n-content="autoclickDelayExtremelyShort"></option> + <option value="400" + i18n-content="autoclickDelayVeryShort"></option> + <option value="600" i18n-content="autoclickDelayShort"></option> + <option value="800" i18n-content="autoclickDelayLong"></option> + <option value="1000" + i18n-content="autoclickDelayVeryLong"></option> + </select> + <span class="controlled-setting-indicator" + pref="settings.a11y.autoclick_delay_ms"> + </span> + </div> + </div> + </span> + </span> + </div> + </div> + </section> +<if expr="pp_ifdef('chromeos')"> + <section id="factory-reset-section" hidden> + <h3 i18n-content="factoryResetTitle"></h3> + <div> + <span class="settings-row" i18n-content="factoryResetDescription"> + </span> + <button id="factory-reset-restart" + i18n-content="factoryResetRestart"> + </button> + </div> + </section> +</if> +</if> +<if expr="not pp_ifdef('chromeos')"> + <section id="system-section"> + <h3 i18n-content="advancedSectionTitleSystem"></h3> +<if expr="not is_macosx"> + <div class="checkbox"> + <span class="controlled-setting-with-label"> + <input id="backgroundModeCheckbox" pref="background_mode.enabled" + type="checkbox"> + <span> + <label for="backgroundModeCheckbox" + i18n-content="backgroundModeCheckbox"> + </label> + <span class="controlled-setting-indicator" + pref="background_mode.enabled"> + </span> + </span> + </span> + </div> +</if> + <div class="checkbox"> + <span class="controlled-setting-with-label"> + <input id="gpu-mode-checkbox" + pref="hardware_acceleration_mode.enabled" type="checkbox"> + <span> + <label for="gpu-mode-checkbox" + i18n-content="gpuModeCheckbox"> + </label> + <span id="gpu-mode-reset-restart" + i18n-values=".innerHTML:gpuModeResetRestart"> + </span> + </span> + </span> + </div> + </section> +</if> + <section id="reset-profile-settings-section" hidden> + <h3 i18n-content="resetProfileSettingsSectionTitle"></h3> + <div> + <span class="settings-row" i18n-content="resetProfileSettingsDescription"> + </span> + <button id="reset-profile-settings" i18n-content="resetProfileSettings"> + </button> + </div> + </section> + </div> <!-- advanced-settings-container --> + </div> <!-- advanced-settings --> + <footer> + <button id="advanced-settings-expander" class="link-button" + i18n-content="showAdvancedSettings"> + </button> + </footer> + </div> diff --git a/chromium/chrome/browser/resources/options/browser_options.js b/chromium/chrome/browser/resources/options/browser_options.js new file mode 100644 index 00000000000..60cacd18495 --- /dev/null +++ b/chromium/chrome/browser/resources/options/browser_options.js @@ -0,0 +1,1621 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + var OptionsPage = options.OptionsPage; + var ArrayDataModel = cr.ui.ArrayDataModel; + var RepeatingButton = cr.ui.RepeatingButton; + + // + // BrowserOptions class + // Encapsulated handling of browser options page. + // + function BrowserOptions() { + OptionsPage.call(this, 'settings', loadTimeData.getString('settingsTitle'), + 'settings'); + } + + cr.addSingletonGetter(BrowserOptions); + + BrowserOptions.prototype = { + __proto__: options.OptionsPage.prototype, + + /** + * Keeps track of whether the user is signed in or not. + * @type {boolean} + * @private + */ + signedIn_: false, + + /** + * Keeps track of whether |onShowHomeButtonChanged_| has been called. See + * |onShowHomeButtonChanged_|. + * @type {boolean} + * @private + */ + onShowHomeButtonChangedCalled_: false, + + /** + * Track if page initialization is complete. All C++ UI handlers have the + * chance to manipulate page content within their InitializePage methods. + * This flag is set to true after all initializers have been called. + * @type {boolean} + * @private + */ + initializationComplete_: false, + + /** @override */ + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + var self = this; + + // Ensure that navigation events are unblocked on uber page. A reload of + // the settings page while an overlay is open would otherwise leave uber + // page in a blocked state, where tab switching is not possible. + uber.invokeMethodOnParent('stopInterceptingEvents'); + + window.addEventListener('message', this.handleWindowMessage_.bind(this)); + + $('advanced-settings-expander').onclick = function() { + self.toggleSectionWithAnimation_( + $('advanced-settings'), + $('advanced-settings-container')); + + // If the link was focused (i.e., it was activated using the keyboard) + // and it was used to show the section (rather than hiding it), focus + // the first element in the container. + if (document.activeElement === $('advanced-settings-expander') && + $('advanced-settings').style.height === '') { + var focusElement = $('advanced-settings-container').querySelector( + 'button, input, list, select, a[href]'); + if (focusElement) + focusElement.focus(); + } + } + + $('advanced-settings').addEventListener('webkitTransitionEnd', + this.updateAdvancedSettingsExpander_.bind(this)); + + if (cr.isChromeOS) + UIAccountTweaks.applyGuestModeVisibility(document); + + // Sync (Sign in) section. + this.updateSyncState_(loadTimeData.getValue('syncData')); + + $('start-stop-sync').onclick = function(event) { + if (self.signedIn_) + SyncSetupOverlay.showStopSyncingUI(); + else if (cr.isChromeOS) + SyncSetupOverlay.showSetupUI(); + else + SyncSetupOverlay.startSignIn(); + }; + $('customize-sync').onclick = function(event) { + SyncSetupOverlay.showSetupUI(); + }; + + // Internet connection section (ChromeOS only). + if (cr.isChromeOS) { + options.network.NetworkList.decorate($('network-list')); + options.network.NetworkList.refreshNetworkData( + loadTimeData.getValue('networkData')); + } + + // On Startup section. + Preferences.getInstance().addEventListener('session.restore_on_startup', + this.onRestoreOnStartupChanged_.bind(this)); + Preferences.getInstance().addEventListener( + 'session.startup_urls', + function(event) { + $('startup-set-pages').disabled = event.value.disabled; + }); + + $('startup-set-pages').onclick = function() { + OptionsPage.navigateToPage('startup'); + }; + + // Appearance section. + Preferences.getInstance().addEventListener('browser.show_home_button', + this.onShowHomeButtonChanged_.bind(this)); + + Preferences.getInstance().addEventListener('homepage', + this.onHomePageChanged_.bind(this)); + Preferences.getInstance().addEventListener('homepage_is_newtabpage', + this.onHomePageIsNtpChanged_.bind(this)); + + $('change-home-page').onclick = function(event) { + OptionsPage.navigateToPage('homePageOverlay'); + }; + + if ($('set-wallpaper')) { + $('set-wallpaper').onclick = function(event) { + chrome.send('openWallpaperManager'); + }; + } + + $('themes-gallery').onclick = function(event) { + window.open(loadTimeData.getString('themesGalleryURL')); + }; + $('themes-reset').onclick = function(event) { + chrome.send('themesReset'); + }; + + if (loadTimeData.getBoolean('profileIsManaged')) { + if ($('themes-native-button')) { + $('themes-native-button').disabled = true; + $('themes-native-button').hidden = true; + } + // Supervised users have just one default theme, even on Linux. So use + // the same button for Linux as for the other platforms. + $('themes-reset').textContent = loadTimeData.getString('themesReset'); + } + + // Device section (ChromeOS only). + if (cr.isChromeOS) { + $('keyboard-settings-button').onclick = function(evt) { + OptionsPage.navigateToPage('keyboard-overlay'); + }; + $('pointer-settings-button').onclick = function(evt) { + OptionsPage.navigateToPage('pointer-overlay'); + }; + } + + // Search section. + $('manage-default-search-engines').onclick = function(event) { + OptionsPage.navigateToPage('searchEngines'); + chrome.send('coreOptionsUserMetricsAction', + ['Options_ManageSearchEngines']); + }; + $('default-search-engine').addEventListener('change', + this.setDefaultSearchEngine_); + // Without this, the bubble would overlap the uber frame navigation pane + // and would not get mouse event as explained in crbug.com/311421. + document.querySelector( + '#default-search-engine + .controlled-setting-indicator').location = + cr.ui.ArrowLocation.TOP_START; + + // Users section. + if (loadTimeData.valueExists('profilesInfo')) { + $('profiles-section').hidden = false; + + var profilesList = $('profiles-list'); + options.browser_options.ProfileList.decorate(profilesList); + profilesList.autoExpands = true; + + // The profiles info data in |loadTimeData| might be stale. + this.setProfilesInfo_(loadTimeData.getValue('profilesInfo')); + chrome.send('requestProfilesInfo'); + + profilesList.addEventListener('change', + this.setProfileViewButtonsStatus_); + $('profiles-create').onclick = function(event) { + ManageProfileOverlay.showCreateDialog(); + }; + if (OptionsPage.isSettingsApp()) { + $('profiles-app-list-switch').onclick = function(event) { + var selectedProfile = self.getSelectedProfileItem_(); + chrome.send('switchAppListProfile', [selectedProfile.filePath]); + }; + } + $('profiles-manage').onclick = function(event) { + ManageProfileOverlay.showManageDialog(); + }; + $('profiles-delete').onclick = function(event) { + var selectedProfile = self.getSelectedProfileItem_(); + if (selectedProfile) + ManageProfileOverlay.showDeleteDialog(selectedProfile); + }; + if (loadTimeData.getBoolean('profileIsManaged')) { + $('profiles-create').disabled = true; + $('profiles-delete').disabled = true; + $('profiles-list').canDeleteItems = false; + } + } + + if (cr.isChromeOS) { + // Username (canonical email) of the currently logged in user or + // |kGuestUser| if a guest session is active. + this.username_ = loadTimeData.getString('username'); + + this.updateAccountPicture_(); + + $('account-picture').onclick = this.showImagerPickerOverlay_; + $('change-picture-caption').onclick = this.showImagerPickerOverlay_; + + $('manage-accounts-button').onclick = function(event) { + OptionsPage.navigateToPage('accounts'); + chrome.send('coreOptionsUserMetricsAction', + ['Options_ManageAccounts']); + }; + } else { + $('import-data').onclick = function(event) { + ImportDataOverlay.show(); + chrome.send('coreOptionsUserMetricsAction', ['Import_ShowDlg']); + }; + + if ($('themes-native-button')) { + $('themes-native-button').onclick = function(event) { + chrome.send('themesSetNative'); + }; + } + } + + // Default browser section. + if (!cr.isChromeOS) { + $('set-as-default-browser').onclick = function(event) { + chrome.send('becomeDefaultBrowser'); + }; + + $('auto-launch').onclick = this.handleAutoLaunchChanged_; + } + + // Privacy section. + $('privacyContentSettingsButton').onclick = function(event) { + OptionsPage.navigateToPage('content'); + OptionsPage.showTab($('cookies-nav-tab')); + chrome.send('coreOptionsUserMetricsAction', + ['Options_ContentSettings']); + }; + $('privacyClearDataButton').onclick = function(event) { + OptionsPage.navigateToPage('clearBrowserData'); + chrome.send('coreOptionsUserMetricsAction', ['Options_ClearData']); + }; + $('privacyClearDataButton').hidden = OptionsPage.isSettingsApp(); + // 'metricsReportingEnabled' element is only present on Chrome branded + // builds, and the 'metricsReportingCheckboxAction' message is only + // handled on ChromeOS. + if ($('metricsReportingEnabled') && cr.isChromeOS) { + $('metricsReportingEnabled').onclick = function(event) { + chrome.send('metricsReportingCheckboxAction', + [String(event.currentTarget.checked)]); + }; + } + + // Bluetooth (CrOS only). + if (cr.isChromeOS) { + options.system.bluetooth.BluetoothDeviceList.decorate( + $('bluetooth-paired-devices-list')); + + $('bluetooth-add-device').onclick = + this.handleAddBluetoothDevice_.bind(this); + + $('enable-bluetooth').onchange = function(event) { + var state = $('enable-bluetooth').checked; + chrome.send('bluetoothEnableChange', [Boolean(state)]); + }; + + $('bluetooth-reconnect-device').onclick = function(event) { + var device = $('bluetooth-paired-devices-list').selectedItem; + var address = device.address; + chrome.send('updateBluetoothDevice', [address, 'connect']); + OptionsPage.closeOverlay(); + }; + + $('bluetooth-paired-devices-list').addEventListener('change', + function() { + var item = $('bluetooth-paired-devices-list').selectedItem; + var disabled = !item || item.connected || !item.connectable; + $('bluetooth-reconnect-device').disabled = disabled; + }); + } + + // Passwords and Forms section. + $('autofill-settings').onclick = function(event) { + OptionsPage.navigateToPage('autofill'); + chrome.send('coreOptionsUserMetricsAction', + ['Options_ShowAutofillSettings']); + }; + $('manage-passwords').onclick = function(event) { + OptionsPage.navigateToPage('passwords'); + OptionsPage.showTab($('passwords-nav-tab')); + chrome.send('coreOptionsUserMetricsAction', + ['Options_ShowPasswordManager']); + }; + if (cr.isChromeOS && UIAccountTweaks.loggedInAsGuest()) { + // Disable and turn off Autofill in guest mode. + var autofillEnabled = $('autofill-enabled'); + autofillEnabled.disabled = true; + autofillEnabled.checked = false; + cr.dispatchSimpleEvent(autofillEnabled, 'change'); + $('autofill-settings').disabled = true; + + // Disable and turn off Password Manager in guest mode. + var passwordManagerEnabled = $('password-manager-enabled'); + passwordManagerEnabled.disabled = true; + passwordManagerEnabled.checked = false; + cr.dispatchSimpleEvent(passwordManagerEnabled, 'change'); + $('manage-passwords').disabled = true; + } + + if (cr.isMac) { + $('mac-passwords-warning').hidden = + !loadTimeData.getBoolean('multiple_profiles'); + } + + // Network section. + if (!cr.isChromeOS) { + $('proxiesConfigureButton').onclick = function(event) { + chrome.send('showNetworkProxySettings'); + }; + } + + // Web Content section. + $('fontSettingsCustomizeFontsButton').onclick = function(event) { + OptionsPage.navigateToPage('fonts'); + chrome.send('coreOptionsUserMetricsAction', ['Options_FontSettings']); + }; + $('defaultFontSize').onchange = function(event) { + var value = event.target.options[event.target.selectedIndex].value; + Preferences.setIntegerPref( + 'webkit.webprefs.default_fixed_font_size', + value - OptionsPage.SIZE_DIFFERENCE_FIXED_STANDARD, true); + chrome.send('defaultFontSizeAction', [String(value)]); + }; + $('defaultZoomFactor').onchange = function(event) { + chrome.send('defaultZoomFactorAction', + [String(event.target.options[event.target.selectedIndex].value)]); + }; + + // Languages section. + var showLanguageOptions = function(event) { + OptionsPage.navigateToPage('languages'); + chrome.send('coreOptionsUserMetricsAction', + ['Options_LanuageAndSpellCheckSettings']); + }; + $('language-button').onclick = showLanguageOptions; + $('manage-languages').onclick = showLanguageOptions; + + // Downloads section. + Preferences.getInstance().addEventListener('download.default_directory', + this.onDefaultDownloadDirectoryChanged_.bind(this)); + $('downloadLocationChangeButton').onclick = function(event) { + chrome.send('selectDownloadLocation'); + }; + if (!cr.isChromeOS) { + $('autoOpenFileTypesResetToDefault').onclick = function(event) { + chrome.send('autoOpenFileTypesAction'); + }; + } else { + $('disable-drive-row').hidden = + UIAccountTweaks.loggedInAsLocallyManagedUser(); + } + + // HTTPS/SSL section. + if (cr.isWindows || cr.isMac) { + $('certificatesManageButton').onclick = function(event) { + chrome.send('showManageSSLCertificates'); + }; + } else { + $('certificatesManageButton').onclick = function(event) { + OptionsPage.navigateToPage('certificates'); + chrome.send('coreOptionsUserMetricsAction', + ['Options_ManageSSLCertificates']); + }; + } + + // Cloud Print section. + // 'cloudPrintProxyEnabled' is true for Chrome branded builds on + // certain platforms, or could be enabled by a lab. + if (!cr.isChromeOS) { + $('cloudPrintConnectorSetupButton').onclick = function(event) { + if ($('cloudPrintManageButton').style.display == 'none') { + // Disable the button, set its text to the intermediate state. + $('cloudPrintConnectorSetupButton').textContent = + loadTimeData.getString('cloudPrintConnectorEnablingButton'); + $('cloudPrintConnectorSetupButton').disabled = true; + chrome.send('showCloudPrintSetupDialog'); + } else { + chrome.send('disableCloudPrintConnector'); + } + }; + } + $('cloudPrintManageButton').onclick = function(event) { + chrome.send('showCloudPrintManagePage'); + }; + + if (loadTimeData.getBoolean('cloudPrintShowMDnsOptions')) { + $('cloudprint-options-mdns').hidden = false; + $('cloudprint-options-nomdns').hidden = true; + $('cloudPrintDevicesPageButton').onclick = function() { + chrome.send('showCloudPrintDevicesPage'); + }; + } + + // Accessibility section (CrOS only). + if (cr.isChromeOS) { + var updateAccessibilitySettingsButton = function() { + $('accessibility-settings').hidden = + !($('accessibility-spoken-feedback-check').checked); + }; + Preferences.getInstance().addEventListener( + 'settings.accessibility', + updateAccessibilitySettingsButton); + $('accessibility-settings-button').onclick = function(event) { + window.open(loadTimeData.getString('accessibilitySettingsURL')); + }; + $('accessibility-spoken-feedback-check').onchange = function(event) { + chrome.send('spokenFeedbackChange', + [$('accessibility-spoken-feedback-check').checked]); + updateAccessibilitySettingsButton(); + }; + updateAccessibilitySettingsButton(); + + $('accessibility-high-contrast-check').onchange = function(event) { + chrome.send('highContrastChange', + [$('accessibility-high-contrast-check').checked]); + }; + + var updateDelayDropdown = function() { + $('accessibility-autoclick-dropdown').disabled = + !$('accessibility-autoclick-check').checked; + }; + Preferences.getInstance().addEventListener( + $('accessibility-autoclick-check').getAttribute('pref'), + updateDelayDropdown); + + $('accessibility-sticky-keys').hidden = + !loadTimeData.getBoolean('enableStickyKeys'); + } + + // Display management section (CrOS only). + if (cr.isChromeOS) { + $('display-options').onclick = function(event) { + OptionsPage.navigateToPage('display'); + chrome.send('coreOptionsUserMetricsAction', + ['Options_Display']); + }; + } + + // Factory reset section (CrOS only). + if (cr.isChromeOS) { + $('factory-reset-restart').onclick = function(event) { + OptionsPage.navigateToPage('factoryResetData'); + }; + } + + // System section. + if (!cr.isChromeOS) { + var updateGpuRestartButton = function() { + $('gpu-mode-reset-restart').hidden = + loadTimeData.getBoolean('gpuEnabledAtStart') == + $('gpu-mode-checkbox').checked; + }; + Preferences.getInstance().addEventListener( + $('gpu-mode-checkbox').getAttribute('pref'), + updateGpuRestartButton); + $('gpu-mode-reset-restart-button').onclick = function(event) { + chrome.send('restartBrowser'); + }; + updateGpuRestartButton(); + } + + // Reset profile settings section. + $('reset-profile-settings').onclick = function(event) { + OptionsPage.navigateToPage('resetProfileSettings'); + }; + $('reset-profile-settings-section').hidden = + !loadTimeData.getBoolean('enableResetProfileSettings'); + }, + + /** @override */ + didShowPage: function() { + $('search-field').focus(); + }, + + /** + * Called after all C++ UI handlers have called InitializePage to notify + * that initialization is complete. + * @private + */ + notifyInitializationComplete_: function() { + this.initializationComplete_ = true; + cr.dispatchSimpleEvent(document, 'initializationComplete'); + }, + + /** + * Event listener for the 'session.restore_on_startup' pref. + * @param {Event} event The preference change event. + * @private + */ + onRestoreOnStartupChanged_: function(event) { + /** @const */ var showHomePageValue = 0; + + if (event.value.value == showHomePageValue) { + // If the user previously selected "Show the homepage", the + // preference will already be migrated to "Open a specific page". So + // the only way to reach this code is if the 'restore on startup' + // preference is managed. + assert(event.value.controlledBy); + + // Select "open the following pages" and lock down the list of URLs + // to reflect the intention of the policy. + $('startup-show-pages').checked = true; + StartupOverlay.getInstance().setControlsDisabled(true); + } else { + // Re-enable the controls in the startup overlay if necessary. + StartupOverlay.getInstance().updateControlStates(); + } + }, + + /** + * Handler for messages sent from the main uber page. + * @param {Event} e The 'message' event from the uber page. + * @private + */ + handleWindowMessage_: function(e) { + if (e.data.method == 'frameSelected') + $('search-field').focus(); + }, + + /** + * Shows the given section. + * @param {HTMLElement} section The section to be shown. + * @param {HTMLElement} container The container for the section. Must be + * inside of |section|. + * @param {boolean} animate Indicate if the expansion should be animated. + * @private + */ + showSection_: function(section, container, animate) { + if (animate) + this.addTransitionEndListener_(section); + + // Unhide + section.hidden = false; + section.style.height = '0px'; + + var expander = function() { + // Reveal the section using a WebKit transition if animating. + if (animate) { + section.classList.add('sliding'); + section.style.height = container.offsetHeight + 'px'; + } else { + section.style.height = 'auto'; + } + }; + + // Delay starting the transition if animating so that hidden change will + // be processed. + if (animate) + setTimeout(expander, 0); + else + expander(); + }, + + /** + * Shows the given section, with animation. + * @param {HTMLElement} section The section to be shown. + * @param {HTMLElement} container The container for the section. Must be + * inside of |section|. + * @private + */ + showSectionWithAnimation_: function(section, container) { + this.showSection_(section, container, /*animate */ true); + }, + + /** + * See showSectionWithAnimation_. + */ + hideSectionWithAnimation_: function(section, container) { + this.addTransitionEndListener_(section); + + // Before we start hiding the section, we need to set + // the height to a pixel value. + section.style.height = container.offsetHeight + 'px'; + + // Delay starting the transition so that the height change will be + // processed. + setTimeout(function() { + // Hide the section using a WebKit transition. + section.classList.add('sliding'); + section.style.height = '0px'; + }, 0); + }, + + /** + * See showSectionWithAnimation_. + */ + toggleSectionWithAnimation_: function(section, container) { + if (section.style.height == '') + this.showSectionWithAnimation_(section, container); + else + this.hideSectionWithAnimation_(section, container); + }, + + /** + * Scrolls the settings page to make the section visible auto-expanding + * advanced settings if required. The transition is not animated. This + * method is used to ensure that a section associated with an overlay + * is visible when the overlay is closed. + * @param {!Element} section The section to make visible. + * @private + */ + scrollToSection_: function(section) { + var advancedSettings = $('advanced-settings'); + var container = $('advanced-settings-container'); + if (advancedSettings.hidden && section.parentNode == container) { + this.showSection_($('advanced-settings'), + $('advanced-settings-container'), + /* animate */ false); + this.updateAdvancedSettingsExpander_(); + } + + if (!this.initializationComplete_) { + var self = this; + var callback = function() { + document.removeEventListener('initializationComplete', callback); + self.scrollToSection_(section); + }; + document.addEventListener('initializationComplete', callback); + return; + } + + var pageContainer = $('page-container'); + // pageContainer.offsetTop is relative to the screen. + var pageTop = pageContainer.offsetTop; + var sectionBottom = section.offsetTop + section.offsetHeight; + // section.offsetTop is relative to the 'page-container'. + var sectionTop = section.offsetTop; + if (pageTop + sectionBottom > document.body.scrollHeight || + pageTop + sectionTop < 0) { + // Currently not all layout updates are guaranteed to precede the + // initializationComplete event (for example 'set-as-default-browser' + // button) leaving some uncertainty in the optimal scroll position. + // The section is placed approximately in the middle of the screen. + var top = Math.min(0, document.body.scrollHeight / 2 - sectionBottom); + pageContainer.style.top = top + 'px'; + pageContainer.oldScrollTop = -top; + } + }, + + /** + * Adds a |webkitTransitionEnd| listener to the given section so that + * it can be animated. The listener will only be added to a given section + * once, so this can be called as multiple times. + * @param {HTMLElement} section The section to be animated. + * @private + */ + addTransitionEndListener_: function(section) { + if (section.hasTransitionEndListener_) + return; + + section.addEventListener('webkitTransitionEnd', + this.onTransitionEnd_.bind(this)); + section.hasTransitionEndListener_ = true; + }, + + /** + * Called after an animation transition has ended. + * @private + */ + onTransitionEnd_: function(event) { + if (event.propertyName != 'height') + return; + + var section = event.target; + + // Disable WebKit transitions. + section.classList.remove('sliding'); + + if (section.style.height == '0px') { + // Hide the content so it can't get tab focus. + section.hidden = true; + section.style.height = ''; + } else { + // Set the section height to 'auto' to allow for size changes + // (due to font change or dynamic content). + section.style.height = 'auto'; + } + }, + + updateAdvancedSettingsExpander_: function() { + var expander = $('advanced-settings-expander'); + if ($('advanced-settings').style.height == '') + expander.textContent = loadTimeData.getString('showAdvancedSettings'); + else + expander.textContent = loadTimeData.getString('hideAdvancedSettings'); + }, + + /** + * Updates the sync section with the given state. + * @param {Object} syncData A bunch of data records that describe the status + * of the sync system. + * @private + */ + updateSyncState_: function(syncData) { + if (!syncData.signinAllowed && + (!syncData.supervisedUser || !cr.isChromeOS)) { + $('sync-section').hidden = true; + return; + } + + $('sync-section').hidden = false; + + var subSection = $('sync-section').firstChild; + while (subSection) { + if (subSection.nodeType == Node.ELEMENT_NODE) + subSection.hidden = syncData.supervisedUser; + subSection = subSection.nextSibling; + } + + if (syncData.supervisedUser) { + $('account-picture-wrapper').hidden = false; + $('sync-general').hidden = false; + $('sync-status').hidden = true; + return; + } + + // If the user gets signed out while the advanced sync settings dialog is + // visible, say, due to a dashboard clear, close the dialog. + // However, if the user gets signed out as a result of abandoning first + // time sync setup, do not call closeOverlay as it will redirect the + // browser to the main settings page and override any in-progress + // user-initiated navigation. See crbug.com/278030. + // Note: SyncSetupOverlay.closeOverlay is a no-op if the overlay is + // already hidden. + if (this.signedIn_ && !syncData.signedIn && !syncData.setupInProgress) + SyncSetupOverlay.closeOverlay(); + + this.signedIn_ = syncData.signedIn; + + // Display the "advanced settings" button if we're signed in and sync is + // not managed/disabled. If the user is signed in, but sync is disabled, + // this button is used to re-enable sync. + var customizeSyncButton = $('customize-sync'); + customizeSyncButton.hidden = !this.signedIn_ || + syncData.managed || + !syncData.syncSystemEnabled; + + // Only modify the customize button's text if the new text is different. + // Otherwise, it can affect search-highlighting in the settings page. + // See http://crbug.com/268265. + var customizeSyncButtonNewText = syncData.setupCompleted ? + loadTimeData.getString('customizeSync') : + loadTimeData.getString('syncButtonTextStart'); + if (customizeSyncButton.textContent != customizeSyncButtonNewText) + customizeSyncButton.textContent = customizeSyncButtonNewText; + + // Disable the "sign in" button if we're currently signing in, or if we're + // already signed in and signout is not allowed. + var signInButton = $('start-stop-sync'); + signInButton.disabled = syncData.setupInProgress || + !syncData.signoutAllowed; + if (!syncData.signoutAllowed) + $('start-stop-sync-indicator').setAttribute('controlled-by', 'policy'); + else + $('start-stop-sync-indicator').removeAttribute('controlled-by'); + + // Hide the "sign in" button on Chrome OS, and show it on desktop Chrome. + signInButton.hidden = cr.isChromeOS; + + signInButton.textContent = + this.signedIn_ ? + loadTimeData.getString('syncButtonTextStop') : + syncData.setupInProgress ? + loadTimeData.getString('syncButtonTextInProgress') : + loadTimeData.getString('syncButtonTextSignIn'); + $('start-stop-sync-indicator').hidden = signInButton.hidden; + + // TODO(estade): can this just be textContent? + $('sync-status-text').innerHTML = syncData.statusText; + var statusSet = syncData.statusText.length != 0; + $('sync-overview').hidden = statusSet; + $('sync-status').hidden = !statusSet; + + $('sync-action-link').textContent = syncData.actionLinkText; + // Don't show the action link if it is empty or undefined. + $('sync-action-link').hidden = syncData.actionLinkText.length == 0; + $('sync-action-link').disabled = syncData.managed || + !syncData.syncSystemEnabled; + + // On Chrome OS, sign out the user and sign in again to get fresh + // credentials on auth errors. + $('sync-action-link').onclick = function(event) { + if (cr.isChromeOS && syncData.hasError) + SyncSetupOverlay.doSignOutOnAuthError(); + else + SyncSetupOverlay.showSetupUI(); + }; + + if (syncData.hasError) + $('sync-status').classList.add('sync-error'); + else + $('sync-status').classList.remove('sync-error'); + + // Disable the "customize / set up sync" button if sync has an + // unrecoverable error. Also disable the button if sync has not been set + // up and the user is being presented with a link to re-auth. + // See crbug.com/289791. + customizeSyncButton.disabled = + syncData.hasUnrecoverableError || + (!syncData.setupCompleted && !$('sync-action-link').hidden); + + // Move #enable-auto-login-checkbox to a different location on CrOS. + if (cr.isChromeOs) { + $('sync-general').insertBefore($('sync-status').nextSibling, + $('enable-auto-login-checkbox')); + } + $('enable-auto-login-checkbox').hidden = !syncData.autoLoginVisible; + }, + + /** + * Update the UI depending on whether the current profile manages any + * supervised users. + * @param {boolean} value True if the current profile manages any supervised + * users. + */ + updateManagesSupervisedUsers_: function(value) { + $('profiles-supervised-dashboard-tip').hidden = !value; + }, + + /** + * Get the start/stop sync button DOM element. Used for testing. + * @return {DOMElement} The start/stop sync button. + * @private + */ + getStartStopSyncButton_: function() { + return $('start-stop-sync'); + }, + + /** + * Event listener for the 'show home button' preference. Shows/hides the + * UI for changing the home page with animation, unless this is the first + * time this function is called, in which case there is no animation. + * @param {Event} event The preference change event. + */ + onShowHomeButtonChanged_: function(event) { + var section = $('change-home-page-section'); + if (this.onShowHomeButtonChangedCalled_) { + var container = $('change-home-page-section-container'); + if (event.value.value) + this.showSectionWithAnimation_(section, container); + else + this.hideSectionWithAnimation_(section, container); + } else { + section.hidden = !event.value.value; + this.onShowHomeButtonChangedCalled_ = true; + } + }, + + /** + * Event listener for the 'homepage is NTP' preference. Updates the label + * next to the 'Change' button. + * @param {Event} event The preference change event. + */ + onHomePageIsNtpChanged_: function(event) { + if (!event.value.uncommitted) { + $('home-page-url').hidden = event.value.value; + $('home-page-ntp').hidden = !event.value.value; + } + }, + + /** + * Event listener for changes to the homepage preference. Updates the label + * next to the 'Change' button. + * @param {Event} event The preference change event. + */ + onHomePageChanged_: function(event) { + if (!event.value.uncommitted) + $('home-page-url').textContent = this.stripHttp_(event.value.value); + }, + + /** + * Removes the 'http://' from a URL, like the omnibox does. If the string + * doesn't start with 'http://' it is returned unchanged. + * @param {string} url The url to be processed + * @return {string} The url with the 'http://' removed. + */ + stripHttp_: function(url) { + return url.replace(/^http:\/\//, ''); + }, + + /** + * Shows the autoLaunch preference and initializes its checkbox value. + * @param {bool} enabled Whether autolaunch is enabled or or not. + * @private + */ + updateAutoLaunchState_: function(enabled) { + $('auto-launch-option').hidden = false; + $('auto-launch').checked = enabled; + }, + + /** + * Called when the value of the download.default_directory preference + * changes. + * @param {Event} event Change event. + * @private + */ + onDefaultDownloadDirectoryChanged_: function(event) { + $('downloadLocationPath').value = event.value.value; + if (cr.isChromeOS) { + // On ChromeOS, replace /special/drive/root with Drive for drive paths, + // /home/chronos/user/Downloads or /home/chronos/u-<hash>/Downloads + // with Downloads for local paths, and '/' with ' \u203a ' (angled quote + // sign) everywhere. The modified path is used only for display purpose. + var path = $('downloadLocationPath').value; + path = path.replace(/^\/special\/drive\/root/, 'Google Drive'); + path = path.replace(/^\/home\/chronos\/(user|u-[^\/]*)\//, ''); + path = path.replace(/\//g, ' \u203a '); + $('downloadLocationPath').value = path; + } + $('download-location-label').classList.toggle('disabled', + event.value.disabled); + $('downloadLocationChangeButton').disabled = event.value.disabled; + }, + + /** + * Update the Default Browsers section based on the current state. + * @param {string} statusString Description of the current default state. + * @param {boolean} isDefault Whether or not the browser is currently + * default. + * @param {boolean} canBeDefault Whether or not the browser can be default. + * @private + */ + updateDefaultBrowserState_: function(statusString, isDefault, + canBeDefault) { + if (!cr.isChromeOS) { + var label = $('default-browser-state'); + label.textContent = statusString; + + $('set-as-default-browser').hidden = !canBeDefault || isDefault; + } + }, + + /** + * Clears the search engine popup. + * @private + */ + clearSearchEngines_: function() { + $('default-search-engine').textContent = ''; + }, + + /** + * Updates the search engine popup with the given entries. + * @param {Array} engines List of available search engines. + * @param {number} defaultValue The value of the current default engine. + * @param {boolean} defaultManaged Whether the default search provider is + * managed. If true, the default search provider can't be changed. + * @private + */ + updateSearchEngines_: function(engines, defaultValue, defaultManaged) { + this.clearSearchEngines_(); + engineSelect = $('default-search-engine'); + engineSelect.disabled = defaultManaged; + if (defaultManaged && defaultValue == -1) + return; + engineCount = engines.length; + var defaultIndex = -1; + for (var i = 0; i < engineCount; i++) { + var engine = engines[i]; + var option = new Option(engine.name, engine.index); + if (defaultValue == option.value) + defaultIndex = i; + engineSelect.appendChild(option); + } + if (defaultIndex >= 0) + engineSelect.selectedIndex = defaultIndex; + }, + + /** + * Set the default search engine based on the popup selection. + * @private + */ + setDefaultSearchEngine_: function() { + var engineSelect = $('default-search-engine'); + var selectedIndex = engineSelect.selectedIndex; + if (selectedIndex >= 0) { + var selection = engineSelect.options[selectedIndex]; + chrome.send('setDefaultSearchEngine', [String(selection.value)]); + } + }, + + /** + * Sets or clear whether Chrome should Auto-launch on computer startup. + * @private + */ + handleAutoLaunchChanged_: function() { + chrome.send('toggleAutoLaunch', [$('auto-launch').checked]); + }, + + /** + * Get the selected profile item from the profile list. This also works + * correctly if the list is not displayed. + * @return {Object} the profile item object, or null if nothing is selected. + * @private + */ + getSelectedProfileItem_: function() { + var profilesList = $('profiles-list'); + if (profilesList.hidden) { + if (profilesList.dataModel.length > 0) + return profilesList.dataModel.item(0); + } else { + return profilesList.selectedItem; + } + return null; + }, + + /** + * Helper function to set the status of profile view buttons to disabled or + * enabled, depending on the number of profiles and selection status of the + * profiles list. + * @private + */ + setProfileViewButtonsStatus_: function() { + var profilesList = $('profiles-list'); + var selectedProfile = profilesList.selectedItem; + var hasSelection = selectedProfile != null; + var hasSingleProfile = profilesList.dataModel.length == 1; + var isManaged = loadTimeData.getBoolean('profileIsManaged'); + $('profiles-manage').disabled = !hasSelection || + !selectedProfile.isCurrentProfile; + if (hasSelection && !selectedProfile.isCurrentProfile) + $('profiles-manage').title = loadTimeData.getString('currentUserOnly'); + else + $('profiles-manage').title = ''; + $('profiles-delete').disabled = isManaged || + (!hasSelection && !hasSingleProfile); + if (OptionsPage.isSettingsApp()) { + $('profiles-app-list-switch').disabled = !hasSelection || + selectedProfile.isCurrentProfile; + } + var importData = $('import-data'); + if (importData) { + importData.disabled = $('import-data').disabled = hasSelection && + !selectedProfile.isCurrentProfile; + } + }, + + /** + * Display the correct dialog layout, depending on how many profiles are + * available. + * @param {number} numProfiles The number of profiles to display. + * @private + */ + setProfileViewSingle_: function(numProfiles) { + var hasSingleProfile = numProfiles == 1; + $('profiles-list').hidden = hasSingleProfile; + $('profiles-single-message').hidden = !hasSingleProfile; + $('profiles-manage').hidden = + hasSingleProfile || OptionsPage.isSettingsApp(); + $('profiles-delete').textContent = hasSingleProfile ? + loadTimeData.getString('profilesDeleteSingle') : + loadTimeData.getString('profilesDelete'); + if (OptionsPage.isSettingsApp()) + $('profiles-app-list-switch').hidden = hasSingleProfile; + }, + + /** + * Adds all |profiles| to the list. + * @param {Array.<Object>} profiles An array of profile info objects. + * each object is of the form: + * profileInfo = { + * name: "Profile Name", + * iconURL: "chrome://path/to/icon/image", + * filePath: "/path/to/profile/data/on/disk", + * isCurrentProfile: false + * }; + * @private + */ + setProfilesInfo_: function(profiles) { + this.setProfileViewSingle_(profiles.length); + // add it to the list, even if the list is hidden so we can access it + // later. + $('profiles-list').dataModel = new ArrayDataModel(profiles); + + // Received new data. If showing the "manage" overlay, keep it up to + // date. If showing the "delete" overlay, close it. + if (ManageProfileOverlay.getInstance().visible && + !$('manage-profile-overlay-manage').hidden) { + ManageProfileOverlay.showManageDialog(); + } else { + ManageProfileOverlay.getInstance().visible = false; + } + + this.setProfileViewButtonsStatus_(); + }, + + /** + * Reports managed user import errors to the ManagedUserImportOverlay. + * @param {string} error The error message to display. + * @private + */ + showManagedUserImportError_: function(error) { + ManagedUserImportOverlay.onError(error); + }, + + /** + * Reports successful importing of a managed user to + * the ManagedUserImportOverlay. + * @private + */ + showManagedUserImportSuccess_: function() { + ManagedUserImportOverlay.onSuccess(); + }, + + /** + * Reports an error to the "create" overlay during profile creation. + * @param {string} error The error message to display. + * @private + */ + showCreateProfileError_: function(error) { + CreateProfileOverlay.onError(error); + }, + + /** + * Sends a warning message to the "create" overlay during profile creation. + * @param {string} warning The warning message to display. + * @private + */ + showCreateProfileWarning_: function(warning) { + CreateProfileOverlay.onWarning(warning); + }, + + /** + * Reports successful profile creation to the "create" overlay. + * @param {Object} profileInfo An object of the form: + * profileInfo = { + * name: "Profile Name", + * filePath: "/path/to/profile/data/on/disk" + * isManaged: (true|false), + * }; + * @private + */ + showCreateProfileSuccess_: function(profileInfo) { + CreateProfileOverlay.onSuccess(profileInfo); + }, + + /** + * Returns the currently active profile for this browser window. + * @return {Object} A profile info object. + * @private + */ + getCurrentProfile_: function() { + for (var i = 0; i < $('profiles-list').dataModel.length; i++) { + var profile = $('profiles-list').dataModel.item(i); + if (profile.isCurrentProfile) + return profile; + } + + assert(false, + 'There should always be a current profile, but none found.'); + }, + + setNativeThemeButtonEnabled_: function(enabled) { + var button = $('themes-native-button'); + if (button) + button.disabled = !enabled; + }, + + setThemesResetButtonEnabled_: function(enabled) { + $('themes-reset').disabled = !enabled; + }, + + setAccountPictureManaged_: function(managed) { + var picture = $('account-picture'); + if (managed || UIAccountTweaks.loggedInAsGuest()) { + picture.disabled = true; + ChangePictureOptions.closeOverlay(); + } else { + picture.disabled = false; + } + + // Create a synthetic pref change event decorated as + // CoreOptionsHandler::CreateValueForPref() does. + var event = new Event('account-picture'); + if (managed) + event.value = { controlledBy: 'policy' }; + else + event.value = {}; + $('account-picture-indicator').handlePrefChange(event); + }, + + /** + * (Re)loads IMG element with current user account picture. + * @private + */ + updateAccountPicture_: function() { + var picture = $('account-picture'); + if (picture) { + picture.src = 'chrome://userimage/' + this.username_ + '?id=' + + Date.now(); + } + }, + + /** + * Handle the 'add device' button click. + * @private + */ + handleAddBluetoothDevice_: function() { + chrome.send('findBluetoothDevices'); + OptionsPage.showPageByName('bluetooth', false); + }, + + /** + * Enables or disables the Manage SSL Certificates button. + * @private + */ + enableCertificateButton_: function(enabled) { + $('certificatesManageButton').disabled = !enabled; + }, + + /** + * Enables factory reset section. + * @private + */ + enableFactoryResetSection_: function() { + $('factory-reset-section').hidden = false; + }, + + /** + * Set the checked state of the metrics reporting checkbox. + * @private + */ + setMetricsReportingCheckboxState_: function(checked, disabled) { + $('metricsReportingEnabled').checked = checked; + $('metricsReportingEnabled').disabled = disabled; + }, + + /** + * @private + */ + setMetricsReportingSettingVisibility_: function(visible) { + if (visible) + $('metricsReportingSetting').style.display = 'block'; + else + $('metricsReportingSetting').style.display = 'none'; + }, + + /** + * Set the visibility of the password generation checkbox. + * @private + */ + setPasswordGenerationSettingVisibility_: function(visible) { + if (visible) + $('password-generation-checkbox').style.display = 'block'; + else + $('password-generation-checkbox').style.display = 'none'; + }, + + /** + * Set the font size selected item. This item actually reflects two + * preferences: the default font size and the default fixed font size. + * + * @param {Object} pref Information about the font size preferences. + * @param {number} pref.value The value of the default font size pref. + * @param {boolean} pref.disabled True if either pref not user modifiable. + * @param {string} pref.controlledBy The source of the pref value(s) if + * either pref is currently not controlled by the user. + * @private + */ + setFontSize_: function(pref) { + var selectCtl = $('defaultFontSize'); + selectCtl.disabled = pref.disabled; + // Create a synthetic pref change event decorated as + // CoreOptionsHandler::CreateValueForPref() does. + var event = new Event('synthetic-font-size'); + event.value = { + value: pref.value, + controlledBy: pref.controlledBy, + disabled: pref.disabled + }; + $('font-size-indicator').handlePrefChange(event); + + for (var i = 0; i < selectCtl.options.length; i++) { + if (selectCtl.options[i].value == pref.value) { + selectCtl.selectedIndex = i; + if ($('Custom')) + selectCtl.remove($('Custom').index); + return; + } + } + + // Add/Select Custom Option in the font size label list. + if (!$('Custom')) { + var option = new Option(loadTimeData.getString('fontSizeLabelCustom'), + -1, false, true); + option.setAttribute('id', 'Custom'); + selectCtl.add(option); + } + $('Custom').selected = true; + }, + + /** + * Populate the page zoom selector with values received from the caller. + * @param {Array} items An array of items to populate the selector. + * each object is an array with three elements as follows: + * 0: The title of the item (string). + * 1: The value of the item (number). + * 2: Whether the item should be selected (boolean). + * @private + */ + setupPageZoomSelector_: function(items) { + var element = $('defaultZoomFactor'); + + // Remove any existing content. + element.textContent = ''; + + // Insert new child nodes into select element. + var value, title, selected; + for (var i = 0; i < items.length; i++) { + title = items[i][0]; + value = items[i][1]; + selected = items[i][2]; + element.appendChild(new Option(title, value, false, selected)); + } + }, + + /** + * Shows/hides the autoOpenFileTypesResetToDefault button and label, with + * animation. + * @param {boolean} display Whether to show the button and label or not. + * @private + */ + setAutoOpenFileTypesDisplayed_: function(display) { + if (cr.isChromeOS) + return; + + if ($('advanced-settings').hidden) { + // If the Advanced section is hidden, don't animate the transition. + $('auto-open-file-types-section').hidden = !display; + } else { + if (display) { + this.showSectionWithAnimation_( + $('auto-open-file-types-section'), + $('auto-open-file-types-container')); + } else { + this.hideSectionWithAnimation_( + $('auto-open-file-types-section'), + $('auto-open-file-types-container')); + } + } + }, + + /** + * Set the enabled state for the proxy settings button. + * @private + */ + setupProxySettingsSection_: function(disabled, extensionControlled) { + if (!cr.isChromeOS) { + $('proxiesConfigureButton').disabled = disabled; + $('proxiesLabel').textContent = + loadTimeData.getString(extensionControlled ? + 'proxiesLabelExtension' : 'proxiesLabelSystem'); + } + }, + + /** + * Set the Cloud Print proxy UI to enabled, disabled, or processing. + * @private + */ + setupCloudPrintConnectorSection_: function(disabled, label, allowed) { + if (!cr.isChromeOS) { + $('cloudPrintConnectorLabel').textContent = label; + if (disabled || !allowed) { + $('cloudPrintConnectorSetupButton').textContent = + loadTimeData.getString('cloudPrintConnectorDisabledButton'); + $('cloudPrintManageButton').style.display = 'none'; + } else { + $('cloudPrintConnectorSetupButton').textContent = + loadTimeData.getString('cloudPrintConnectorEnabledButton'); + $('cloudPrintManageButton').style.display = 'inline'; + } + $('cloudPrintConnectorSetupButton').disabled = !allowed; + } + }, + + /** + * @private + */ + removeCloudPrintConnectorSection_: function() { + if (!cr.isChromeOS) { + var connectorSectionElm = $('cloud-print-connector-section'); + if (connectorSectionElm) + connectorSectionElm.parentNode.removeChild(connectorSectionElm); + } + }, + + /** + * Set the initial state of the spoken feedback checkbox. + * @private + */ + setSpokenFeedbackCheckboxState_: function(checked) { + $('accessibility-spoken-feedback-check').checked = checked; + }, + + /** + * Set the initial state of the high contrast checkbox. + * @private + */ + setHighContrastCheckboxState_: function(checked) { + $('accessibility-high-contrast-check').checked = checked; + }, + + /** + * Set the initial state of the virtual keyboard checkbox. + * @private + */ + setVirtualKeyboardCheckboxState_: function(checked) { + // TODO(zork): Update UI + }, + + /** + * Show/hide mouse settings slider. + * @private + */ + showMouseControls_: function(show) { + $('mouse-settings').hidden = !show; + }, + + /** + * Show/hide touchpad-related settings. + * @private + */ + showTouchpadControls_: function(show) { + $('touchpad-settings').hidden = !show; + $('accessibility-tap-dragging').hidden = !show; + }, + + /** + * Activate the Bluetooth settings section on the System settings page. + * @private + */ + showBluetoothSettings_: function() { + $('bluetooth-devices').hidden = false; + }, + + /** + * Dectivates the Bluetooth settings section from the System settings page. + * @private + */ + hideBluetoothSettings_: function() { + $('bluetooth-devices').hidden = true; + }, + + /** + * Sets the state of the checkbox indicating if Bluetooth is turned on. The + * state of the "Find devices" button and the list of discovered devices may + * also be affected by a change to the state. + * @param {boolean} checked Flag Indicating if Bluetooth is turned on. + * @private + */ + setBluetoothState_: function(checked) { + $('enable-bluetooth').checked = checked; + $('bluetooth-paired-devices-list').parentNode.hidden = !checked; + $('bluetooth-add-device').hidden = !checked; + $('bluetooth-reconnect-device').hidden = !checked; + // Flush list of previously discovered devices if bluetooth is turned off. + if (!checked) { + $('bluetooth-paired-devices-list').clear(); + $('bluetooth-unpaired-devices-list').clear(); + } else { + chrome.send('getPairedBluetoothDevices'); + } + }, + + /** + * Adds an element to the list of available Bluetooth devices. If an element + * with a matching address is found, the existing element is updated. + * @param {{name: string, + * address: string, + * paired: boolean, + * connected: boolean}} device + * Decription of the Bluetooth device. + * @private + */ + addBluetoothDevice_: function(device) { + var list = $('bluetooth-unpaired-devices-list'); + // Display the "connecting" (already paired or not yet paired) and the + // paired devices in the same list. + if (device.paired || device.connecting) { + // Test to see if the device is currently in the unpaired list, in which + // case it should be removed from that list. + var index = $('bluetooth-unpaired-devices-list').find(device.address); + if (index != undefined) + $('bluetooth-unpaired-devices-list').deleteItemAtIndex(index); + list = $('bluetooth-paired-devices-list'); + } else { + // Test to see if the device is currently in the paired list, in which + // case it should be removed from that list. + var index = $('bluetooth-paired-devices-list').find(device.address); + if (index != undefined) + $('bluetooth-paired-devices-list').deleteItemAtIndex(index); + } + list.appendDevice(device); + + // One device can be in the process of pairing. If found, display + // the Bluetooth pairing overlay. + if (device.pairing) + BluetoothPairing.showDialog(device); + }, + + /** + * Removes an element from the list of available devices. + * @param {string} address Unique address of the device. + * @private + */ + removeBluetoothDevice_: function(address) { + var index = $('bluetooth-unpaired-devices-list').find(address); + if (index != undefined) { + $('bluetooth-unpaired-devices-list').deleteItemAtIndex(index); + } else { + index = $('bluetooth-paired-devices-list').find(address); + if (index != undefined) + $('bluetooth-paired-devices-list').deleteItemAtIndex(index); + } + }, + + /** + * Shows the overlay dialog for changing the user avatar image. + * @private + */ + showImagerPickerOverlay_: function() { + OptionsPage.navigateToPage('changePicture'); + } + }; + + //Forward public APIs to private implementations. + [ + 'addBluetoothDevice', + 'enableCertificateButton', + 'enableFactoryResetSection', + 'getCurrentProfile', + 'getStartStopSyncButton', + 'hideBluetoothSettings', + 'notifyInitializationComplete', + 'removeBluetoothDevice', + 'removeCloudPrintConnectorSection', + 'scrollToSection', + 'setAccountPictureManaged', + 'setAutoOpenFileTypesDisplayed', + 'setBluetoothState', + 'setFontSize', + 'setNativeThemeButtonEnabled', + 'setHighContrastCheckboxState', + 'setMetricsReportingCheckboxState', + 'setMetricsReportingSettingVisibility', + 'setPasswordGenerationSettingVisibility', + 'setProfilesInfo', + 'setSpokenFeedbackCheckboxState', + 'setThemesResetButtonEnabled', + 'setVirtualKeyboardCheckboxState', + 'setupCloudPrintConnectorSection', + 'setupPageZoomSelector', + 'setupProxySettingsSection', + 'showBluetoothSettings', + 'showCreateProfileError', + 'showCreateProfileSuccess', + 'showCreateProfileWarning', + 'showManagedUserImportError', + 'showManagedUserImportSuccess', + 'showMouseControls', + 'showTouchpadControls', + 'updateAccountPicture', + 'updateAutoLaunchState', + 'updateDefaultBrowserState', + 'updateManagesSupervisedUsers', + 'updateSearchEngines', + 'updateStartupPages', + 'updateSyncState', + ].forEach(function(name) { + BrowserOptions[name] = function() { + var instance = BrowserOptions.getInstance(); + return instance[name + '_'].apply(instance, arguments); + }; + }); + + if (cr.isChromeOS) { + /** + * Returns username (canonical email) of the user logged in (ChromeOS only). + * @return {string} user email. + */ + // TODO(jhawkins): Investigate the use case for this method. + BrowserOptions.getLoggedInUsername = function() { + return BrowserOptions.getInstance().username_; + }; + } + + // Export + return { + BrowserOptions: BrowserOptions + }; +}); diff --git a/chromium/chrome/browser/resources/options/browser_options_profile_list.js b/chromium/chrome/browser/resources/options/browser_options_profile_list.js new file mode 100644 index 00000000000..7a60ca3cf16 --- /dev/null +++ b/chromium/chrome/browser/resources/options/browser_options_profile_list.js @@ -0,0 +1,129 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options.browser_options', function() { + /** @const */ var DeletableItem = options.DeletableItem; + /** @const */ var DeletableItemList = options.DeletableItemList; + /** @const */ var ListSingleSelectionModel = cr.ui.ListSingleSelectionModel; + + /** + * Creates a new profile list item. + * @param {Object} profileInfo The profile this item respresents. + * @constructor + * @extends {cr.ui.DeletableItem} + */ + function ProfileListItem(profileInfo) { + var el = cr.doc.createElement('div'); + el.profileInfo_ = profileInfo; + ProfileListItem.decorate(el); + return el; + } + + /** + * Decorates an element as a profile list item. + * @param {!HTMLElement} el The element to decorate. + */ + ProfileListItem.decorate = function(el) { + el.__proto__ = ProfileListItem.prototype; + el.decorate(); + }; + + ProfileListItem.prototype = { + __proto__: DeletableItem.prototype, + + /** + * @type {string} the file path of this profile list item. + */ + get profilePath() { + return this.profileInfo_.filePath; + }, + + /** + * @type {boolean} whether this profile is managed. + */ + get isManaged() { + return this.profileInfo_.isManaged; + }, + + /** @override */ + decorate: function() { + DeletableItem.prototype.decorate.call(this); + + var profileInfo = this.profileInfo_; + + var iconEl = this.ownerDocument.createElement('img'); + iconEl.className = 'profile-img'; + iconEl.style.content = imageset(profileInfo.iconURL + '@scalefactorx'); + this.contentElement.appendChild(iconEl); + + var nameEl = this.ownerDocument.createElement('div'); + nameEl.className = 'profile-name'; + if (profileInfo.isCurrentProfile) + nameEl.classList.add('profile-item-current'); + this.contentElement.appendChild(nameEl); + + var displayName = profileInfo.name; + if (profileInfo.isCurrentProfile) { + displayName = loadTimeData.getStringF('profilesListItemCurrent', + profileInfo.name); + } + nameEl.textContent = displayName; + + // Ensure that the button cannot be tabbed to for accessibility reasons. + this.closeButtonElement.tabIndex = -1; + }, + }; + + var ProfileList = cr.ui.define('list'); + + ProfileList.prototype = { + __proto__: DeletableItemList.prototype, + + /** @override */ + decorate: function() { + DeletableItemList.prototype.decorate.call(this); + this.selectionModel = new ListSingleSelectionModel(); + }, + + /** @override */ + createItem: function(pageInfo) { + var item = new ProfileListItem(pageInfo); + item.deletable = this.canDeleteItems_; + return item; + }, + + /** @override */ + deleteItemAtIndex: function(index) { + if (loadTimeData.getBoolean('profileIsManaged')) + return; + ManageProfileOverlay.showDeleteDialog(this.dataModel.item(index)); + }, + + /** @override */ + activateItemAtIndex: function(index) { + // Don't allow the user to edit a profile that is not current. + var profileInfo = this.dataModel.item(index); + if (profileInfo.isCurrentProfile) + ManageProfileOverlay.showManageDialog(profileInfo); + }, + + /** + * Sets whether items in this list are deletable. + */ + set canDeleteItems(value) { + this.canDeleteItems_ = value; + }, + + /** + * If false, items in this list will not be deltable. + * @private + */ + canDeleteItems_: true, + }; + + return { + ProfileList: ProfileList + }; +}); + diff --git a/chromium/chrome/browser/resources/options/browser_options_startup_page_list.js b/chromium/chrome/browser/resources/options/browser_options_startup_page_list.js new file mode 100644 index 00000000000..6afed9a081d --- /dev/null +++ b/chromium/chrome/browser/resources/options/browser_options_startup_page_list.js @@ -0,0 +1,321 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options.browser_options', function() { + /** @const */ var AutocompleteList = cr.ui.AutocompleteList; + /** @const */ var InlineEditableItem = options.InlineEditableItem; + /** @const */ var InlineEditableItemList = options.InlineEditableItemList; + + /** + * Creates a new startup page list item. + * @param {Object} pageInfo The page this item represents. + * @constructor + * @extends {cr.ui.ListItem} + */ + function StartupPageListItem(pageInfo) { + var el = cr.doc.createElement('div'); + el.pageInfo_ = pageInfo; + StartupPageListItem.decorate(el); + return el; + } + + /** + * Decorates an element as a startup page list item. + * @param {!HTMLElement} el The element to decorate. + */ + StartupPageListItem.decorate = function(el) { + el.__proto__ = StartupPageListItem.prototype; + el.decorate(); + }; + + StartupPageListItem.prototype = { + __proto__: InlineEditableItem.prototype, + + /** + * Input field for editing the page url. + * @type {HTMLElement} + * @private + */ + urlField_: null, + + /** @override */ + decorate: function() { + InlineEditableItem.prototype.decorate.call(this); + + var pageInfo = this.pageInfo_; + + if (pageInfo.modelIndex == -1) { + this.isPlaceholder = true; + pageInfo.title = loadTimeData.getString('startupAddLabel'); + pageInfo.url = ''; + } + + var titleEl = this.ownerDocument.createElement('div'); + titleEl.className = 'title'; + titleEl.classList.add('favicon-cell'); + titleEl.classList.add('weakrtl'); + titleEl.textContent = pageInfo.title; + if (!this.isPlaceholder) { + titleEl.style.backgroundImage = getFaviconImageSet(pageInfo.url); + titleEl.title = pageInfo.tooltip; + } + + this.contentElement.appendChild(titleEl); + + var urlEl = this.createEditableTextCell(pageInfo.url); + urlEl.className = 'url'; + urlEl.classList.add('weakrtl'); + this.contentElement.appendChild(urlEl); + + var urlField = urlEl.querySelector('input'); + urlField.className = 'weakrtl'; + urlField.placeholder = loadTimeData.getString('startupPagesPlaceholder'); + this.urlField_ = urlField; + + this.addEventListener('commitedit', this.onEditCommitted_); + + var self = this; + urlField.addEventListener('focus', function(event) { + self.parentNode.autocompleteList.attachToInput(urlField); + }); + urlField.addEventListener('blur', function(event) { + self.parentNode.autocompleteList.detach(); + }); + + if (!this.isPlaceholder) + this.draggable = true; + }, + + /** @override */ + get currentInputIsValid() { + return this.urlField_.validity.valid; + }, + + /** @override */ + get hasBeenEdited() { + return this.urlField_.value != this.pageInfo_.url; + }, + + /** + * Called when committing an edit; updates the model. + * @param {Event} e The end event. + * @private + */ + onEditCommitted_: function(e) { + var url = this.urlField_.value; + if (this.isPlaceholder) + chrome.send('addStartupPage', [url]); + else + chrome.send('editStartupPage', [this.pageInfo_.modelIndex, url]); + }, + }; + + var StartupPageList = cr.ui.define('list'); + + StartupPageList.prototype = { + __proto__: InlineEditableItemList.prototype, + + /** + * An autocomplete suggestion list for URL editing. + * @type {AutocompleteList} + */ + autocompleteList: null, + + /** + * The drop position information: "below" or "above". + */ + dropPos: null, + + /** @override */ + decorate: function() { + InlineEditableItemList.prototype.decorate.call(this); + + // Listen to drag and drop events. + this.addEventListener('dragstart', this.handleDragStart_.bind(this)); + this.addEventListener('dragenter', this.handleDragEnter_.bind(this)); + this.addEventListener('dragover', this.handleDragOver_.bind(this)); + this.addEventListener('drop', this.handleDrop_.bind(this)); + this.addEventListener('dragleave', this.handleDragLeave_.bind(this)); + this.addEventListener('dragend', this.handleDragEnd_.bind(this)); + }, + + /** @override */ + createItem: function(pageInfo) { + var item = new StartupPageListItem(pageInfo); + item.urlField_.disabled = this.disabled; + return item; + }, + + /** @override */ + deleteItemAtIndex: function(index) { + chrome.send('removeStartupPages', [index]); + }, + + /** + * Computes the target item of drop event. + * @param {Event} e The drop or dragover event. + * @private + */ + getTargetFromDropEvent_: function(e) { + var target = e.target; + // e.target may be an inner element of the list item + while (target != null && !(target instanceof StartupPageListItem)) { + target = target.parentNode; + } + return target; + }, + + /** + * Handles the dragstart event. + * @param {Event} e The dragstart event. + * @private + */ + handleDragStart_: function(e) { + // Prevent dragging if the list is disabled. + if (this.disabled) { + e.preventDefault(); + return false; + } + + var target = e.target; + // StartupPageListItem should be the only draggable element type in the + // page but let's make sure. + if (target instanceof StartupPageListItem) { + this.draggedItem = target; + this.draggedItem.editable = false; + e.dataTransfer.effectAllowed = 'move'; + // We need to put some kind of data in the drag or it will be + // ignored. Use the URL in case the user drags to a text field or the + // desktop. + e.dataTransfer.setData('text/plain', target.urlField_.value); + } + }, + + /* + * Handles the dragenter event. + * @param {Event} e The dragenter event. + * @private + */ + handleDragEnter_: function(e) { + e.preventDefault(); + }, + + /* + * Handles the dragover event. + * @param {Event} e The dragover event. + * @private + */ + handleDragOver_: function(e) { + var dropTarget = this.getTargetFromDropEvent_(e); + // Determines whether the drop target is to accept the drop. + // The drop is only successful on another StartupPageListItem. + if (!(dropTarget instanceof StartupPageListItem) || + dropTarget == this.draggedItem || dropTarget.isPlaceholder) { + this.hideDropMarker_(); + return; + } + // Compute the drop postion. Should we move the dragged item to + // below or above the drop target? + var rect = dropTarget.getBoundingClientRect(); + var dy = e.clientY - rect.top; + var yRatio = dy / rect.height; + var dropPos = yRatio <= .5 ? 'above' : 'below'; + this.dropPos = dropPos; + this.showDropMarker_(dropTarget, dropPos); + e.preventDefault(); + }, + + /* + * Handles the drop event. + * @param {Event} e The drop event. + * @private + */ + handleDrop_: function(e) { + var dropTarget = this.getTargetFromDropEvent_(e); + + if (!(dropTarget instanceof StartupPageListItem) || + dropTarget.pageInfo_.modelIndex == -1) { + return; + } + + this.hideDropMarker_(); + + // Insert the selection at the new position. + var newIndex = this.dataModel.indexOf(dropTarget.pageInfo_); + if (this.dropPos == 'below') + newIndex += 1; + + // If there are selected indexes, it was a re-order. + if (this.selectionModel.selectedIndexes.length > 0) { + chrome.send('dragDropStartupPage', + [newIndex, this.selectionModel.selectedIndexes]); + return; + } + + // Otherwise it was potentially a drop of new data (e.g. a bookmark). + var url = e.dataTransfer.getData('url'); + if (url) { + e.preventDefault(); + chrome.send('addStartupPage', [url, newIndex]); + } + }, + + /** + * Handles the dragleave event. + * @param {Event} e The dragleave event. + * @private + */ + handleDragLeave_: function(e) { + this.hideDropMarker_(); + }, + + /** + * Handles the dragend event. + * @param {Event} e The dragend event. + * @private + */ + handleDragEnd_: function(e) { + this.draggedItem.editable = true; + this.draggedItem.updateEditState(); + }, + + /** + * Shows and positions the marker to indicate the drop target. + * @param {HTMLElement} target The current target list item of drop. + * @param {string} pos 'below' or 'above'. + * @private + */ + showDropMarker_: function(target, pos) { + window.clearTimeout(this.hideDropMarkerTimer_); + var marker = $('startupPagesListDropmarker'); + var rect = target.getBoundingClientRect(); + var markerHeight = 6; + if (pos == 'above') { + marker.style.top = (rect.top - markerHeight / 2) + 'px'; + } else { + marker.style.top = (rect.bottom - markerHeight / 2) + 'px'; + } + marker.style.width = rect.width + 'px'; + marker.style.left = rect.left + 'px'; + marker.style.display = 'block'; + }, + + /** + * Hides the drop marker. + * @private + */ + hideDropMarker_: function() { + // Hide the marker in a timeout to reduce flickering as we move between + // valid drop targets. + window.clearTimeout(this.hideDropMarkerTimer_); + this.hideDropMarkerTimer_ = window.setTimeout(function() { + $('startupPagesListDropmarker').style.display = ''; + }, 100); + }, + }; + + return { + StartupPageList: StartupPageList + }; +}); diff --git a/chromium/chrome/browser/resources/options/certificate_backup_overlay.html b/chromium/chrome/browser/resources/options/certificate_backup_overlay.html new file mode 100644 index 00000000000..89cdba866d1 --- /dev/null +++ b/chromium/chrome/browser/resources/options/certificate_backup_overlay.html @@ -0,0 +1,40 @@ +<div id="certificateBackupOverlay" class="page" hidden> + <div class="close-button"></div> + <h1 i18n-content="certificateExportPasswordDescription"></h1> + <div class="content-area"> + <table> + <tr> + <td> + <label for="certificateBackupPassword"> + <span i18n-content="certificatePasswordLabel"></span> + </label> + </td> + <td> + <input id="certificateBackupPassword" type="password"> + </td> + </tr> + <tr> + <td> + <label for="certificateBackupPassword2"> + <span i18n-content="certificateConfirmPasswordLabel"></span> + </label> + </td> + <td> + <input id="certificateBackupPassword2" type="password"> + </td> + </tr> + </table> + <p> + <span i18n-content="certificateExportPasswordHelp"></span> + </p> + </div> + <div class="action-area"> + <div class="button-strip"> + <button id="certificateBackupCancelButton" type="reset" + i18n-content="cancel"></button> + <button id="certificateBackupOkButton" class="default-button" + type="submit" i18n-content="ok" disabled> + </button> + </div> + </div> +</div> diff --git a/chromium/chrome/browser/resources/options/certificate_backup_overlay.js b/chromium/chrome/browser/resources/options/certificate_backup_overlay.js new file mode 100644 index 00000000000..c904a47f9b9 --- /dev/null +++ b/chromium/chrome/browser/resources/options/certificate_backup_overlay.js @@ -0,0 +1,117 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + /** @const */ var OptionsPage = options.OptionsPage; + + /** + * CertificateBackupOverlay class + * Encapsulated handling of the 'enter backup password' overlay page. + * @class + */ + function CertificateBackupOverlay() { + OptionsPage.call(this, 'certificateBackupOverlay', + '', + 'certificateBackupOverlay'); + } + + cr.addSingletonGetter(CertificateBackupOverlay); + + CertificateBackupOverlay.prototype = { + __proto__: OptionsPage.prototype, + + /** + * Initializes the page. + */ + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + + var self = this; + $('certificateBackupCancelButton').onclick = function(event) { + self.cancelBackup_(); + }; + $('certificateBackupOkButton').onclick = function(event) { + self.finishBackup_(); + }; + var onBackupPasswordInput = function(event) { + self.comparePasswords_(); + }; + $('certificateBackupPassword').oninput = onBackupPasswordInput; + $('certificateBackupPassword2').oninput = onBackupPasswordInput; + + self.clearInputFields_(); + }, + + /** + * Clears any uncommitted input, and dismisses the overlay. + * @private + */ + dismissOverlay_: function() { + this.clearInputFields_(); + OptionsPage.closeOverlay(); + }, + + /** + * Attempt the Backup operation. + * The overlay will be left up with inputs disabled until the backend + * finishes and dismisses it. + * @private + */ + finishBackup_: function() { + chrome.send('exportPersonalCertificatePasswordSelected', + [$('certificateBackupPassword').value]); + $('certificateBackupCancelButton').disabled = true; + $('certificateBackupOkButton').disabled = true; + $('certificateBackupPassword').disabled = true; + $('certificateBackupPassword2').disabled = true; + }, + + /** + * Cancel the Backup operation. + * @private + */ + cancelBackup_: function() { + chrome.send('cancelImportExportCertificate'); + this.dismissOverlay_(); + }, + + /** + * Compares the password fields and sets the button state appropriately. + * @private + */ + comparePasswords_: function() { + var password1 = $('certificateBackupPassword').value; + var password2 = $('certificateBackupPassword2').value; + $('certificateBackupOkButton').disabled = + !password1 || password1 != password2; + }, + + /** + * Clears the value of each input field. + * @private + */ + clearInputFields_: function() { + $('certificateBackupPassword').value = ''; + $('certificateBackupPassword2').value = ''; + $('certificateBackupPassword').disabled = false; + $('certificateBackupPassword2').disabled = false; + $('certificateBackupCancelButton').disabled = false; + $('certificateBackupOkButton').disabled = true; + }, + }; + + CertificateBackupOverlay.show = function() { + CertificateBackupOverlay.getInstance().clearInputFields_(); + OptionsPage.navigateToPage('certificateBackupOverlay'); + }; + + CertificateBackupOverlay.dismiss = function() { + CertificateBackupOverlay.getInstance().dismissOverlay_(); + }; + + // Export + return { + CertificateBackupOverlay: CertificateBackupOverlay + }; +}); diff --git a/chromium/chrome/browser/resources/options/certificate_edit_ca_trust_overlay.html b/chromium/chrome/browser/resources/options/certificate_edit_ca_trust_overlay.html new file mode 100644 index 00000000000..4dabea286a8 --- /dev/null +++ b/chromium/chrome/browser/resources/options/certificate_edit_ca_trust_overlay.html @@ -0,0 +1,39 @@ +<div id="certificateEditCaTrustOverlay" class="page" hidden> + <h1><span i18n-content="certificateEditCaTitle"></span></h1> + <div class="close-button"></div> + <div class="content-area"> + <div> + <span id="certificateEditCaTrustDescription"></span> + </div> + <section> + <h3><span i18n-content="certificateEditTrustLabel"></span></h3> + <div class="checkbox"> + <label id="certificateCaTrustSSLLabel"> + <input id="certificateCaTrustSSLCheckbox" type="checkbox"> + <span i18n-content="certificateCaTrustSSLLabel"></span> + </label> + </div> + <div class="checkbox"> + <label id="certificateCaTrustEmailLabel"> + <input id="certificateCaTrustEmailCheckbox" type="checkbox"> + <span i18n-content="certificateCaTrustEmailLabel"></span> + </label> + </div> + <div class="checkbox"> + <label id="certificateCaTrustObjSignLabel"> + <input id="certificateCaTrustObjSignCheckbox" type="checkbox"> + <span i18n-content="certificateCaTrustObjSignLabel"></span> + </label> + </div> + </section> + </div> + <div class="action-area"> + <div class="button-strip"> + <button id="certificateEditCaTrustCancelButton" type="reset" + i18n-content="cancel"></button> + <button id="certificateEditCaTrustOkButton" class="default-button" + type="submit" i18n-content="ok"> + </button> + </div> + </div> +</div> diff --git a/chromium/chrome/browser/resources/options/certificate_edit_ca_trust_overlay.js b/chromium/chrome/browser/resources/options/certificate_edit_ca_trust_overlay.js new file mode 100644 index 00000000000..5f017c8e932 --- /dev/null +++ b/chromium/chrome/browser/resources/options/certificate_edit_ca_trust_overlay.js @@ -0,0 +1,164 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + /** @const */ var OptionsPage = options.OptionsPage; + + /** + * CertificateEditCaTrustOverlay class + * Encapsulated handling of the 'edit ca trust' and 'import ca' overlay pages. + * @class + */ + function CertificateEditCaTrustOverlay() { + OptionsPage.call(this, 'certificateEditCaTrustOverlay', + '', + 'certificateEditCaTrustOverlay'); + } + + cr.addSingletonGetter(CertificateEditCaTrustOverlay); + + CertificateEditCaTrustOverlay.prototype = { + __proto__: OptionsPage.prototype, + + /** + * Dismisses the overlay. + * @private + */ + dismissOverlay_: function() { + OptionsPage.closeOverlay(); + }, + + /** + * Enables or disables input fields. + * @private + */ + enableInputs_: function(enabled) { + $('certificateCaTrustSSLCheckbox').disabled = + $('certificateCaTrustEmailCheckbox').disabled = + $('certificateCaTrustObjSignCheckbox').disabled = + $('certificateEditCaTrustCancelButton').disabled = + $('certificateEditCaTrustOkButton').disabled = !enabled; + }, + + /** + * Attempt the Edit operation. + * The overlay will be left up with inputs disabled until the backend + * finishes and dismisses it. + * @private + */ + finishEdit_: function() { + // TODO(mattm): Send checked values as booleans. For now send them as + // strings, since WebUIBindings::send does not support any other types :( + chrome.send('editCaCertificateTrust', + [this.certId, + $('certificateCaTrustSSLCheckbox').checked.toString(), + $('certificateCaTrustEmailCheckbox').checked.toString(), + $('certificateCaTrustObjSignCheckbox').checked.toString()]); + this.enableInputs_(false); + }, + + /** + * Cancel the Edit operation. + * @private + */ + cancelEdit_: function() { + this.dismissOverlay_(); + }, + + /** + * Attempt the Import operation. + * The overlay will be left up with inputs disabled until the backend + * finishes and dismisses it. + * @private + */ + finishImport_: function() { + // TODO(mattm): Send checked values as booleans. For now send them as + // strings, since WebUIBindings::send does not support any other types :( + chrome.send('importCaCertificateTrustSelected', + [$('certificateCaTrustSSLCheckbox').checked.toString(), + $('certificateCaTrustEmailCheckbox').checked.toString(), + $('certificateCaTrustObjSignCheckbox').checked.toString()]); + this.enableInputs_(false); + }, + + /** + * Cancel the Import operation. + * @private + */ + cancelImport_: function() { + chrome.send('cancelImportExportCertificate'); + this.dismissOverlay_(); + }, + }; + + /** + * Callback from CertificateManagerHandler with the trust values. + * @param {boolean} trustSSL The initial value of SSL trust checkbox. + * @param {boolean} trustEmail The initial value of Email trust checkbox. + * @param {boolean} trustObjSign The initial value of Object Signing trust. + */ + CertificateEditCaTrustOverlay.populateTrust = function( + trustSSL, trustEmail, trustObjSign) { + $('certificateCaTrustSSLCheckbox').checked = trustSSL; + $('certificateCaTrustEmailCheckbox').checked = trustEmail; + $('certificateCaTrustObjSignCheckbox').checked = trustObjSign; + CertificateEditCaTrustOverlay.getInstance().enableInputs_(true); + } + + /** + * Show the Edit CA Trust overlay. + * @param {string} certId The id of the certificate to be passed to the + * certificate manager model. + * @param {string} certName The display name of the certificate. + * checkbox. + */ + CertificateEditCaTrustOverlay.show = function(certId, certName) { + var self = CertificateEditCaTrustOverlay.getInstance(); + self.certId = certId; + $('certificateEditCaTrustCancelButton').onclick = function(event) { + self.cancelEdit_(); + } + $('certificateEditCaTrustOkButton').onclick = function(event) { + self.finishEdit_(); + } + $('certificateEditCaTrustDescription').textContent = + loadTimeData.getStringF('certificateEditCaTrustDescriptionFormat', + certName); + self.enableInputs_(false); + OptionsPage.navigateToPage('certificateEditCaTrustOverlay'); + chrome.send('getCaCertificateTrust', [certId]); + } + + /** + * Show the Import CA overlay. + * @param {string} certId The id of the certificate to be passed to the + * certificate manager model. + * @param {string} certName The display name of the certificate. + * checkbox. + */ + CertificateEditCaTrustOverlay.showImport = function(certName) { + var self = CertificateEditCaTrustOverlay.getInstance(); + // TODO(mattm): do we want a view certificate button here like firefox has? + $('certificateEditCaTrustCancelButton').onclick = function(event) { + self.cancelImport_(); + } + $('certificateEditCaTrustOkButton').onclick = function(event) { + self.finishImport_(); + } + $('certificateEditCaTrustDescription').textContent = + loadTimeData.getStringF('certificateImportCaDescriptionFormat', + certName); + CertificateEditCaTrustOverlay.populateTrust(false, false, false); + OptionsPage.navigateToPage('certificateEditCaTrustOverlay'); + } + + CertificateEditCaTrustOverlay.dismiss = function() { + CertificateEditCaTrustOverlay.getInstance().dismissOverlay_(); + }; + + // Export + return { + CertificateEditCaTrustOverlay: CertificateEditCaTrustOverlay + }; +}); diff --git a/chromium/chrome/browser/resources/options/certificate_import_error_overlay.html b/chromium/chrome/browser/resources/options/certificate_import_error_overlay.html new file mode 100644 index 00000000000..b36d4db0b83 --- /dev/null +++ b/chromium/chrome/browser/resources/options/certificate_import_error_overlay.html @@ -0,0 +1,15 @@ +<div id="certificateImportErrorOverlay" class="page" hidden> + <div class="close-button"></div> + <h1 id="certificateImportErrorOverlayTitle"></h1> + <div class="content-area"> + <div id="certificateImportErrorOverlayMessage"></div> + <ul id="certificateImportErrorOverlayCertErrors"></ul> + </div> + <div class="action-area"> + <div class="button-strip"> + <button id="certificateImportErrorOverlayOk" class="default-button" + type="submit" i18n-content="done"> + </button> + </div> + </div> +</div> diff --git a/chromium/chrome/browser/resources/options/certificate_import_error_overlay.js b/chromium/chrome/browser/resources/options/certificate_import_error_overlay.js new file mode 100644 index 00000000000..4eed1d25d57 --- /dev/null +++ b/chromium/chrome/browser/resources/options/certificate_import_error_overlay.js @@ -0,0 +1,68 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + + var OptionsPage = options.OptionsPage; + + /** + * CertificateImportErrorOverlay class + * Displays a list of certificates and errors. + * @class + */ + function CertificateImportErrorOverlay() { + OptionsPage.call(this, 'certificateImportErrorOverlay', '', + 'certificateImportErrorOverlay'); + } + + cr.addSingletonGetter(CertificateImportErrorOverlay); + + CertificateImportErrorOverlay.prototype = { + // Inherit CertificateImportErrorOverlay from OptionsPage. + __proto__: OptionsPage.prototype, + + /** + * Initialize the page. + */ + initializePage: function() { + // Call base class implementation to start preference initialization. + OptionsPage.prototype.initializePage.call(this); + + $('certificateImportErrorOverlayOk').onclick = function(event) { + OptionsPage.closeOverlay(); + }; + }, + }; + + /** + * Show an alert overlay with the given message, button titles, and + * callbacks. + * @param {string} title The alert title to display to the user. + * @param {string} message The alert message to display to the user. + * @param {Array} certErrors The list of cert errors. Each error should have + * a .name and .error attribute. + */ + CertificateImportErrorOverlay.show = function(title, message, certErrors) { + $('certificateImportErrorOverlayTitle').textContent = title; + $('certificateImportErrorOverlayMessage').textContent = message; + + ul = $('certificateImportErrorOverlayCertErrors'); + ul.innerHTML = ''; + for (var i = 0; i < certErrors.length; ++i) { + li = document.createElement('li'); + li.textContent = loadTimeData.getStringF('certificateImportErrorFormat', + certErrors[i].name, + certErrors[i].error); + ul.appendChild(li); + } + + OptionsPage.navigateToPage('certificateImportErrorOverlay'); + } + + // Export + return { + CertificateImportErrorOverlay: CertificateImportErrorOverlay + }; + +}); diff --git a/chromium/chrome/browser/resources/options/certificate_manager.css b/chromium/chrome/browser/resources/options/certificate_manager.css new file mode 100644 index 00000000000..0516e5b580a --- /dev/null +++ b/chromium/chrome/browser/resources/options/certificate_manager.css @@ -0,0 +1,32 @@ +/* Copyright (c) 2012 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ + +#certificateManagerPage { + min-width: 700px; +} + +#certificateRestoreOverlay h1 { + /* Leave enough space for the close-button. */ + /* TODO(KGR): make dialogs to use flex-boxes instead of relying on padding. */ + padding-right: 35px; +} + +/* Force tab strip to extend to the left and right edges of the window. */ +#certificate-manager-content-area { + padding: 6px 0 6px 0; +} + +#certificate-manager-content-area .subpages-tab-contents { + padding-left: 28px; + padding-right: 14px; +} + +.certificate-tree-table { + width: 100%; +} + +.certificate-tree { + /* TODO(mattm): BLAH. Make this not statically sized. */ + height: 300px; +} diff --git a/chromium/chrome/browser/resources/options/certificate_manager.html b/chromium/chrome/browser/resources/options/certificate_manager.html new file mode 100644 index 00000000000..3d28e7f2b9f --- /dev/null +++ b/chromium/chrome/browser/resources/options/certificate_manager.html @@ -0,0 +1,144 @@ +<div id="certificateManagerPage" class="page" hidden> + <div class="close-button"></div> + <h1 i18n-content="certificateManagerPage"></h1> + <div id="certificate-manager-content-area" class="content-area"> + <!-- Navigation tabs --> + <div class="subpages-nav-tabs"> + <span id="personal-certs-nav-tab" class="tab" + tab-contents="personalCertsTab"> + <span class="tab-label" i18n-content="personalCertsTabTitle"></span> + <span class="active-tab-label" i18n-content="personalCertsTabTitle"> + </span> + </span> + <span id="server-certs-nav-tab" class="tab" tab-contents="serverCertsTab"> + <span class="tab-label" i18n-content="serverCertsTabTitle"></span> + <span class="active-tab-label" i18n-content="serverCertsTabTitle"> + </span> + </span> + <span id="ca-certs-nav-tab" class="tab" tab-contents="caCertsTab"> + <span class="tab-label" i18n-content="caCertsTabTitle"></span> + <span class="active-tab-label" i18n-content="caCertsTabTitle"></span> + </span> + <span id="other-certs-nav-tab" class="tab" tab-contents="otherCertsTab"> + <span class="tab-label" i18n-content="otherCertsTabTitle"></span> + <span class="active-tab-label" i18n-content="otherCertsTabTitle"> + </span> + </span> + </div> + <!-- TODO(mattm): get rid of use of tables --> + <!-- Tab contents --> + <div id="personalCertsTab" class="subpages-tab-contents"> + <table class="certificate-tree-table"> + <tr><td> + <span i18n-content="personalCertsTabDescription"></span> + </td></tr> + <tr><td> + <tree id="personalCertsTab-tree" class="certificate-tree" + icon-visibility="parent"> + </tree> + </td></tr> + <tr><td> + <button id="personalCertsTab-view" i18n-content="view_certificate" + disabled> + </button> + <button id="personalCertsTab-import" + i18n-content="import_certificate"> + </button> +<if expr="pp_ifdef('chromeos')"> + <button id="personalCertsTab-import-and-bind" + i18n-content="importAndBindCertificate" disabled> + </button> +</if> + <button id="personalCertsTab-backup" i18n-content="export_certificate" + disabled> + </button> + <button id="personalCertsTab-delete" i18n-content="delete_certificate" + disabled> + </button> + </td></tr> + </table> + </div> + <div id="serverCertsTab" class="subpages-tab-contents"> + <table class="certificate-tree-table"> + <tr><td> + <span i18n-content="serverCertsTabDescription"></span> + </td></tr> + <tr><td> + <tree id="serverCertsTab-tree" class="certificate-tree" + icon-visibility="parent"> + </tree> + </td></tr> + <tr><td> + <button id="serverCertsTab-view" i18n-content="view_certificate" + disabled> + </button> + <button id="serverCertsTab-import" i18n-content="import_certificate"> + </button> + <button id="serverCertsTab-export" i18n-content="export_certificate" + disabled> + </button> + <button id="serverCertsTab-delete" i18n-content="delete_certificate" + disabled> + </button> + </td></tr> + </table> + </div> + <div id="caCertsTab" class="subpages-tab-contents"> + <table class="certificate-tree-table"> + <tr><td> + <span i18n-content="caCertsTabDescription"></span> + </td></tr> + <tr><td> + <tree id="caCertsTab-tree" class="certificate-tree" + icon-visibility="parent"> + </tree> + </td></tr> + <tr><td> + <button id="caCertsTab-view" i18n-content="view_certificate" + disabled> + </button> + <button id="caCertsTab-edit" i18n-content="edit_certificate" + disabled> + </button> + <button id="caCertsTab-import" i18n-content="import_certificate" + ></button> + <button id="caCertsTab-export" i18n-content="export_certificate" + disabled> + </button> + <button id="caCertsTab-delete" i18n-content="delete_certificate" + disabled> + </button> + </td></tr> + </table> + </div> + <div id="otherCertsTab" class="subpages-tab-contents"> + <table class="certificate-tree-table"> + <tr><td> + <span i18n-content="otherCertsTabDescription"></span> + </td></tr> + <tr><td> + <tree id="otherCertsTab-tree" class="certificate-tree" + icon-visibility="parent"></tree> + </td></tr> + <tr><td> + <button id="otherCertsTab-view" i18n-content="view_certificate" + disabled> + </button> + <button id="otherCertsTab-export" i18n-content="export_certificate" + disabled> + </button> + <button id="otherCertsTab-delete" i18n-content="delete_certificate" + disabled> + </button> + </td></tr> + </table> + </div> + </div> + <div class="action-area"> + <div class="button-strip"> + <button id="certificate-confirm" class="default-button" + i18n-content="done"> + </button> + </div> + </div> +</div> diff --git a/chromium/chrome/browser/resources/options/certificate_manager.js b/chromium/chrome/browser/resources/options/certificate_manager.js new file mode 100644 index 00000000000..5620b80c6c2 --- /dev/null +++ b/chromium/chrome/browser/resources/options/certificate_manager.js @@ -0,0 +1,256 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + var OptionsPage = options.OptionsPage; + + ///////////////////////////////////////////////////////////////////////////// + // CertificateManagerTab class: + + /** + * blah + * @param {!string} id The id of this tab. + */ + function CertificateManagerTab(id) { + this.tree = $(id + '-tree'); + + options.CertificatesTree.decorate(this.tree); + this.tree.addEventListener('change', + this.handleCertificatesTreeChange_.bind(this)); + + var tree = this.tree; + + this.viewButton = $(id + '-view'); + this.viewButton.onclick = function(e) { + var selected = tree.selectedItem; + chrome.send('viewCertificate', [selected.data.id]); + } + + this.editButton = $(id + '-edit'); + if (this.editButton !== null) { + if (id == 'serverCertsTab') { + this.editButton.onclick = function(e) { + var selected = tree.selectedItem; + chrome.send('editServerCertificate', [selected.data.id]); + } + } else if (id == 'caCertsTab') { + this.editButton.onclick = function(e) { + var data = tree.selectedItem.data; + CertificateEditCaTrustOverlay.show(data.id, data.name); + } + } + } + + this.backupButton = $(id + '-backup'); + if (this.backupButton !== null) { + this.backupButton.onclick = function(e) { + var selected = tree.selectedItem; + chrome.send('exportPersonalCertificate', [selected.data.id]); + } + } + + this.backupAllButton = $(id + '-backup-all'); + if (this.backupAllButton !== null) { + this.backupAllButton.onclick = function(e) { + chrome.send('exportAllPersonalCertificates'); + } + } + + this.importButton = $(id + '-import'); + if (this.importButton !== null) { + if (id == 'personalCertsTab') { + this.importButton.onclick = function(e) { + chrome.send('importPersonalCertificate', [false]); + } + } else if (id == 'serverCertsTab') { + this.importButton.onclick = function(e) { + chrome.send('importServerCertificate'); + } + } else if (id == 'caCertsTab') { + this.importButton.onclick = function(e) { + chrome.send('importCaCertificate'); + } + } + } + + this.importAndBindButton = $(id + '-import-and-bind'); + if (this.importAndBindButton !== null) { + if (id == 'personalCertsTab') { + this.importAndBindButton.onclick = function(e) { + chrome.send('importPersonalCertificate', [true]); + } + } + } + + this.exportButton = $(id + '-export'); + if (this.exportButton !== null) { + this.exportButton.onclick = function(e) { + var selected = tree.selectedItem; + chrome.send('exportCertificate', [selected.data.id]); + } + } + + this.deleteButton = $(id + '-delete'); + this.deleteButton.onclick = function(e) { + var data = tree.selectedItem.data; + AlertOverlay.show( + loadTimeData.getStringF(id + 'DeleteConfirm', data.name), + loadTimeData.getString(id + 'DeleteImpact'), + loadTimeData.getString('ok'), + loadTimeData.getString('cancel'), + function() { + tree.selectedItem = null; + chrome.send('deleteCertificate', [data.id]); + }); + } + } + + CertificateManagerTab.prototype = { + + /** + * Update button state. + * @private + * @param {!Object} data The data of the selected item. + */ + updateButtonState: function(data) { + var isCert = !!data && data.isCert; + var readOnly = !!data && data.readonly; + var extractable = !!data && data.extractable; + var hasChildren = this.tree.items.length > 0; + var isPolicy = !!data && data.policy; + this.viewButton.disabled = !isCert; + if (this.editButton !== null) + this.editButton.disabled = !isCert || isPolicy; + if (this.backupButton !== null) + this.backupButton.disabled = !isCert || !extractable; + if (this.backupAllButton !== null) + this.backupAllButton.disabled = !hasChildren; + if (this.exportButton !== null) + this.exportButton.disabled = !isCert; + this.deleteButton.disabled = !isCert || readOnly || isPolicy; + }, + + /** + * Handles certificate tree selection change. + * @private + * @param {!Event} e The change event object. + */ + handleCertificatesTreeChange_: function(e) { + var data = null; + if (this.tree.selectedItem) { + data = this.tree.selectedItem.data; + } + + this.updateButtonState(data); + }, + }; + + // TODO(xiyuan): Use notification from backend instead of polling. + // TPM token check polling timer. + var tpmPollingTimer; + + // Initiate tpm token check if needed. + function checkTpmToken() { + var importAndBindButton = $('personalCertsTab-import-and-bind'); + + if (importAndBindButton && importAndBindButton.disabled) + chrome.send('checkTpmTokenReady'); + } + + // Stop tpm polling timer. + function stopTpmTokenCheckPolling() { + if (tpmPollingTimer) { + window.clearTimeout(tpmPollingTimer); + tpmPollingTimer = undefined; + } + } + + ///////////////////////////////////////////////////////////////////////////// + // CertificateManager class: + + /** + * Encapsulated handling of ChromeOS accounts options page. + * @constructor + */ + function CertificateManager(model) { + OptionsPage.call(this, 'certificates', + loadTimeData.getString('certificateManagerPageTabTitle'), + 'certificateManagerPage'); + } + + cr.addSingletonGetter(CertificateManager); + + CertificateManager.prototype = { + __proto__: OptionsPage.prototype, + + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + + this.personalTab = new CertificateManagerTab('personalCertsTab'); + this.serverTab = new CertificateManagerTab('serverCertsTab'); + this.caTab = new CertificateManagerTab('caCertsTab'); + this.otherTab = new CertificateManagerTab('otherCertsTab'); + + this.addEventListener('visibleChange', this.handleVisibleChange_); + + $('certificate-confirm').onclick = function() { + OptionsPage.closeOverlay(); + }; + }, + + initalized_: false, + + /** + * Handler for OptionsPage's visible property change event. + * @private + * @param {Event} e Property change event. + */ + handleVisibleChange_: function(e) { + if (!this.initalized_ && this.visible) { + this.initalized_ = true; + OptionsPage.showTab($('personal-certs-nav-tab')); + chrome.send('populateCertificateManager'); + } + + if (cr.isChromeOS) { + // Ensure TPM token check on visible and stop polling when hidden. + if (this.visible) + checkTpmToken(); + else + stopTpmTokenCheckPolling(); + } + } + }; + + // CertificateManagerHandler callbacks. + CertificateManager.onPopulateTree = function(args) { + $(args[0]).populate(args[1]); + }; + + CertificateManager.exportPersonalAskPassword = function(args) { + CertificateBackupOverlay.show(); + }; + + CertificateManager.importPersonalAskPassword = function(args) { + CertificateRestoreOverlay.show(); + }; + + CertificateManager.onCheckTpmTokenReady = function(ready) { + var importAndBindButton = $('personalCertsTab-import-and-bind'); + if (importAndBindButton) { + importAndBindButton.disabled = !ready; + + // Check again after 5 seconds if Tpm is not ready and certificate manager + // is still visible. + if (!ready && CertificateManager.getInstance().visible) + tpmPollingTimer = window.setTimeout(checkTpmToken, 5000); + } + }; + + // Export + return { + CertificateManagerTab: CertificateManagerTab, + CertificateManager: CertificateManager + }; +}); diff --git a/chromium/chrome/browser/resources/options/certificate_restore_overlay.html b/chromium/chrome/browser/resources/options/certificate_restore_overlay.html new file mode 100644 index 00000000000..d68fafb4fee --- /dev/null +++ b/chromium/chrome/browser/resources/options/certificate_restore_overlay.html @@ -0,0 +1,19 @@ +<div id="certificateRestoreOverlay" class="page" hidden> + <div class="close-button"></div> + <h1 i18n-content="certificateRestorePasswordDescription"></h1> + <div class="content-area"> + <label id="certificateRestorePasswordLabel"> + <span i18n-content="certificatePasswordLabel"></span> + <input id="certificateRestorePassword" type="password"> + </label> + </div> + <div class="action-area"> + <div class="button-strip"> + <button id="certificateRestoreCancelButton" type="reset" + i18n-content="cancel"></button> + <button id="certificateRestoreOkButton" class="default-button" + type="submit" i18n-content="ok"> + </button> + </div> + </div> +</div> diff --git a/chromium/chrome/browser/resources/options/certificate_restore_overlay.js b/chromium/chrome/browser/resources/options/certificate_restore_overlay.js new file mode 100644 index 00000000000..08b8ee7920f --- /dev/null +++ b/chromium/chrome/browser/resources/options/certificate_restore_overlay.js @@ -0,0 +1,101 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + /** @const */ var OptionsPage = options.OptionsPage; + + /** + * CertificateRestoreOverlay class + * Encapsulated handling of the 'enter restore password' overlay page. + * @class + */ + function CertificateRestoreOverlay() { + OptionsPage.call(this, 'certificateRestore', '', + 'certificateRestoreOverlay'); + } + + cr.addSingletonGetter(CertificateRestoreOverlay); + + CertificateRestoreOverlay.prototype = { + __proto__: OptionsPage.prototype, + + /** + * Initializes the page. + */ + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + + var self = this; + $('certificateRestoreCancelButton').onclick = function(event) { + self.cancelRestore_(); + }; + $('certificateRestoreOkButton').onclick = function(event) { + self.finishRestore_(); + }; + + self.clearInputFields_(); + }, + + /** @override */ + didShowPage: function() { + $('certificateRestorePassword').focus(); + }, + + /** + * Clears any uncommitted input, and dismisses the overlay. + * @private + */ + dismissOverlay_: function() { + this.clearInputFields_(); + OptionsPage.closeOverlay(); + }, + + /** + * Attempt the restore operation. + * The overlay will be left up with inputs disabled until the backend + * finishes and dismisses it. + * @private + */ + finishRestore_: function() { + chrome.send('importPersonalCertificatePasswordSelected', + [$('certificateRestorePassword').value]); + $('certificateRestoreCancelButton').disabled = true; + $('certificateRestoreOkButton').disabled = true; + }, + + /** + * Cancel the restore operation. + * @private + */ + cancelRestore_: function() { + chrome.send('cancelImportExportCertificate'); + this.dismissOverlay_(); + }, + + /** + * Clears the value of each input field. + * @private + */ + clearInputFields_: function() { + $('certificateRestorePassword').value = ''; + $('certificateRestoreCancelButton').disabled = false; + $('certificateRestoreOkButton').disabled = false; + }, + }; + + CertificateRestoreOverlay.show = function() { + CertificateRestoreOverlay.getInstance().clearInputFields_(); + OptionsPage.navigateToPage('certificateRestore'); + }; + + CertificateRestoreOverlay.dismiss = function() { + CertificateRestoreOverlay.getInstance().dismissOverlay_(); + }; + + // Export + return { + CertificateRestoreOverlay: CertificateRestoreOverlay + }; + +}); diff --git a/chromium/chrome/browser/resources/options/certificate_tree.css b/chromium/chrome/browser/resources/options/certificate_tree.css new file mode 100644 index 00000000000..ee83aed86f8 --- /dev/null +++ b/chromium/chrome/browser/resources/options/certificate_tree.css @@ -0,0 +1,17 @@ +/* Copyright (c) 2012 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ + +span.cert-untrusted { + background-color: pink; + border: 1px solid red; + border-radius: 3px; + margin-right: 3px; + padding-left: 1px; + padding-right: 1px; +} + +span.cert-policy { + margin-left: 3px; + vertical-align: middle; +} diff --git a/chromium/chrome/browser/resources/options/certificate_tree.js b/chromium/chrome/browser/resources/options/certificate_tree.js new file mode 100644 index 00000000000..057e57b9605 --- /dev/null +++ b/chromium/chrome/browser/resources/options/certificate_tree.js @@ -0,0 +1,163 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + /** @const */ var Tree = cr.ui.Tree; + /** @const */ var TreeItem = cr.ui.TreeItem; + + /** + * Creates a new tree folder for certificate data. + * @param {Object=} data Data used to create a certificate tree folder. + * @constructor + * @extends {TreeItem} + */ + function CertificateTreeFolder(data) { + data.isCert = false; + var treeFolder = new TreeItem({ + label: data.name, + data: data + }); + treeFolder.__proto__ = CertificateTreeFolder.prototype; + + if (data.icon) + treeFolder.icon = data.icon; + + return treeFolder; + } + + CertificateTreeFolder.prototype = { + __proto__: TreeItem.prototype, + + /** + * The tree path id/. + * @type {string} + */ + get pathId() { + return this.data.id; + } + }; + + /** + * Creates a new tree item for certificate data. + * @param {Object=} data Data used to create a certificate tree item. + * @constructor + * @extends {TreeItem} + */ + function CertificateTreeItem(data) { + data.isCert = true; + // TODO(mattm): other columns + var treeItem = new TreeItem({ + label: data.name, + data: data + }); + treeItem.__proto__ = CertificateTreeItem.prototype; + + if (data.icon) + treeItem.icon = data.icon; + + if (data.untrusted) { + var badge = document.createElement('span'); + badge.classList.add('cert-untrusted'); + badge.textContent = loadTimeData.getString('badgeCertUntrusted'); + treeItem.labelElement.insertBefore( + badge, treeItem.labelElement.firstChild); + } + + if (data.policy) { + var policyIndicator = new options.ControlledSettingIndicator(); + policyIndicator.controlledBy = 'policy'; + policyIndicator.setAttribute( + 'textpolicy', loadTimeData.getString('certPolicyInstalled')); + policyIndicator.classList.add('cert-policy'); + treeItem.labelElement.appendChild(policyIndicator); + } + + return treeItem; + } + + CertificateTreeItem.prototype = { + __proto__: TreeItem.prototype, + + /** + * The tree path id/. + * @type {string} + */ + get pathId() { + return this.parentItem.pathId + ',' + this.data.id; + } + }; + + /** + * Creates a new cookies tree. + * @param {Object=} opt_propertyBag Optional properties. + * @constructor + * @extends {Tree} + */ + var CertificatesTree = cr.ui.define('tree'); + + CertificatesTree.prototype = { + __proto__: Tree.prototype, + + /** @override */ + decorate: function() { + Tree.prototype.decorate.call(this); + this.treeLookup_ = {}; + }, + + /** @override */ + addAt: function(child, index) { + Tree.prototype.addAt.call(this, child, index); + if (child.data && child.data.id) + this.treeLookup_[child.data.id] = child; + }, + + /** @override */ + remove: function(child) { + Tree.prototype.remove.call(this, child); + if (child.data && child.data.id) + delete this.treeLookup_[child.data.id]; + }, + + /** + * Clears the tree. + */ + clear: function() { + // Remove all fields without recreating the object since other code + // references it. + for (var id in this.treeLookup_) + delete this.treeLookup_[id]; + this.textContent = ''; + }, + + /** + * Populate the tree. + * @param {Array} nodesData Nodes data array. + */ + populate: function(nodesData) { + this.clear(); + + for (var i = 0; i < nodesData.length; ++i) { + var subnodes = nodesData[i].subnodes; + delete nodesData[i].subnodes; + + var item = new CertificateTreeFolder(nodesData[i]); + this.addAt(item, i); + + for (var j = 0; j < subnodes.length; ++j) { + var subitem = new CertificateTreeItem(subnodes[j]); + item.addAt(subitem, j); + } + // Make tree expanded by default. + item.expanded = true; + } + + cr.dispatchSimpleEvent(this, 'change'); + }, + }; + + return { + CertificatesTree: CertificatesTree + }; +}); + diff --git a/chromium/chrome/browser/resources/options/chromeos/2x/warning.png b/chromium/chrome/browser/resources/options/chromeos/2x/warning.png Binary files differnew file mode 100644 index 00000000000..b28ab3643b1 --- /dev/null +++ b/chromium/chrome/browser/resources/options/chromeos/2x/warning.png diff --git a/chromium/chrome/browser/resources/options/chromeos/OWNERS b/chromium/chrome/browser/resources/options/chromeos/OWNERS new file mode 100644 index 00000000000..1b75ad21d63 --- /dev/null +++ b/chromium/chrome/browser/resources/options/chromeos/OWNERS @@ -0,0 +1,15 @@ +achuith@chromium.org +dpolukhin@chromium.org +nkostylev@chromium.org +stevenjb@chromium.org +xiyuan@chromium.org +zelidrag@chromium.org + +# Display options. +mukai@chromium.org + +# Network configuration. +per-file internet_detail*=pneubeck@chromium.org +per-file network_list*=pneubeck@chromium.org +per-file preferred_networks*=pneubeck@chromium.org +per-file proxy_rules_list*=pneubeck@chromium.org diff --git a/chromium/chrome/browser/resources/options/chromeos/accounts_options.html b/chromium/chrome/browser/resources/options/chromeos/accounts_options.html new file mode 100644 index 00000000000..d0fc3bd892b --- /dev/null +++ b/chromium/chrome/browser/resources/options/chromeos/accounts_options.html @@ -0,0 +1,69 @@ +<div id="accountsPage" class="page" hidden> + <div class="close-button"></div> + <h1 i18n-content="accountsPage"></h1> + <div class="content-area"> + <div class="option"> + <div id="ownerOnlyWarning" hidden> + <span i18n-content="owner_only"></span> + <span i18n-content="ownerUserId"></span> + </div> + <table class="option-control-table"> + <tr> + <td class="option-name"> + <div class="checkbox"> + <label> + <input id="allowBwsiCheck" pref="cros.accounts.allowBWSI" + type="checkbox"> + <span i18n-content="allow_BWSI"></span> + </label> + </div> + </td> + </tr> + <tr> + <td class="option-name"> + <div class="checkbox"> + <label> + <input id="showUserNamesCheck" + pref="cros.accounts.showUserNamesOnSignIn" type="checkbox"> + <span i18n-content="show_user_on_signin"></span> + </label> + </div> + </td> + </tr> + <tr> + <td class="option-name"> + <div class="checkbox"> + <label> + <input id="useWhitelistCheck" pref="cros.accounts.allowGuest" + type="checkbox" inverted_pref> + <span i18n-content="use_whitelist"></span> + </label> + </div> + </td> + </tr> + <tr><td> </td></tr> + <tr><td> + <table class="user-list-table"> + <tr><td> + <list id="userList"></list> + </td></tr> + <tr><td class="user-name-edit-row"> + <label><span i18n-content="add_users"></span><br> + <input id="userNameEdit" type="text" + i18n-values="placeholder:username_edit_hint"> + </span> + </label> + </td></tr> + </table> + </td></tr> + </table> + </div> + </div> + <div class="action-area"> + <div class="button-strip"> + <button id="accounts-options-overlay-confirm" class="default-button" + i18n-content="done"> + </button> + </div> + </div> +</div> diff --git a/chromium/chrome/browser/resources/options/chromeos/accounts_options.js b/chromium/chrome/browser/resources/options/chromeos/accounts_options.js new file mode 100644 index 00000000000..eddbbaf3d31 --- /dev/null +++ b/chromium/chrome/browser/resources/options/chromeos/accounts_options.js @@ -0,0 +1,154 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + var OptionsPage = options.OptionsPage; + + ///////////////////////////////////////////////////////////////////////////// + // AccountsOptions class: + + /** + * Encapsulated handling of ChromeOS accounts options page. + * @constructor + */ + function AccountsOptions(model) { + OptionsPage.call(this, 'accounts', + loadTimeData.getString('accountsPageTabTitle'), + 'accountsPage'); + // Whether to show the whitelist. + this.showWhitelist_ = false; + } + + cr.addSingletonGetter(AccountsOptions); + + AccountsOptions.prototype = { + // Inherit AccountsOptions from OptionsPage. + __proto__: OptionsPage.prototype, + + /** + * Initializes AccountsOptions page. + */ + initializePage: function() { + // Call base class implementation to starts preference initialization. + OptionsPage.prototype.initializePage.call(this); + + // Set up accounts page. + var userList = $('userList'); + userList.addEventListener('remove', this.handleRemoveUser_); + + var userNameEdit = $('userNameEdit'); + options.accounts.UserNameEdit.decorate(userNameEdit); + userNameEdit.addEventListener('add', this.handleAddUser_); + + // If the current user is not the owner, do not show the user list. + // If the current user is not the owner, or the device is enterprise + // managed, show a warning that settings cannot be modified. + this.showWhitelist_ = UIAccountTweaks.currentUserIsOwner(); + if (this.showWhitelist_) { + options.accounts.UserList.decorate(userList); + } else { + $('ownerOnlyWarning').hidden = false; + this.managed = AccountsOptions.whitelistIsManaged(); + } + + this.addEventListener('visibleChange', this.handleVisibleChange_); + + $('useWhitelistCheck').addEventListener('change', + this.handleUseWhitelistCheckChange_.bind(this)); + + Preferences.getInstance().addEventListener( + $('useWhitelistCheck').pref, + this.handleUseWhitelistPrefChange_.bind(this)); + + $('accounts-options-overlay-confirm').onclick = + OptionsPage.closeOverlay.bind(OptionsPage); + }, + + /** + * Update user list control state. + * @private + */ + updateControls_: function() { + $('userList').disabled = + $('userNameEdit').disabled = !this.showWhitelist_ || + AccountsOptions.whitelistIsManaged() || + !$('useWhitelistCheck').checked; + }, + + /** + * Handler for OptionsPage's visible property change event. + * @private + * @param {Event} e Property change event. + */ + handleVisibleChange_: function(e) { + if (this.visible) { + this.updateControls_(); + if (this.showWhitelist_) + $('userList').redraw(); + } + }, + + /** + * Handler for allow guest check change. + * @private + */ + handleUseWhitelistCheckChange_: function(e) { + // Whitelist existing users when guest login is being disabled. + if ($('useWhitelistCheck').checked) { + chrome.send('whitelistExistingUsers'); + } + + this.updateControls_(); + }, + + /** + * handler for allow guest pref change. + * @private + */ + handleUseWhitelistPrefChange_: function(e) { + this.updateControls_(); + }, + + /** + * Handler for "add" event fired from userNameEdit. + * @private + * @param {Event} e Add event fired from userNameEdit. + */ + handleAddUser_: function(e) { + chrome.send('whitelistUser', [e.user.email, e.user.name]); + }, + + /** + * Handler for "remove" event fired from userList. + * @private + * @param {Event} e Remove event fired from userList. + */ + handleRemoveUser_: function(e) { + chrome.send('unwhitelistUser', [e.user.username]); + } + }; + + + /** + * Returns whether the whitelist is managed by policy or not. + */ + AccountsOptions.whitelistIsManaged = function() { + return loadTimeData.getBoolean('whitelist_is_managed'); + }; + + /** + * Update account picture. + * @param {string} username User for which to update the image. + */ + AccountsOptions.updateAccountPicture = function(username) { + if (this.showWhitelist_) + $('userList').updateAccountPicture(username); + }; + + // Export + return { + AccountsOptions: AccountsOptions + }; + +}); diff --git a/chromium/chrome/browser/resources/options/chromeos/accounts_options_page.css b/chromium/chrome/browser/resources/options/chromeos/accounts_options_page.css new file mode 100644 index 00000000000..bba94eacfa5 --- /dev/null +++ b/chromium/chrome/browser/resources/options/chromeos/accounts_options_page.css @@ -0,0 +1,98 @@ +/* Copyright (c) 2012 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ + +.user-list-table { + border: 1px solid lightgrey; + border-collapse: collapse; + border-spacing: 0; +} + +.user-name-edit-row { + background-color: rgb(235, 239, 250); + border: 1px solid lightgrey; + padding: 5px; +} + +.user-list-item { + padding: 2px; +} + +.user-icon { + border: 1px solid black; + height: 26px; + width: 26px; +} + +.user-email-label { + -webkit-margin-start: 10px; +} + +.user-name-label { + -webkit-margin-start: 10px; + color: darkgray; +} + +.user-email-name-block { + -webkit-box-flex: 1; + max-width: 318px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.remove-user-button { + background-image: -webkit-image-set( + url('../../../../../ui/resources/default_100_percent/close_2.png') 1x, + url('../../../../../ui/resources/default_200_percent/close_2.png') 2x); + height: 16px; + width: 16px; +} + +.remove-user-button:hover { + background-image: -webkit-image-set( + url('../../../../../ui/resources/default_100_percent/close_2_hover.png') + 1x, + url('../../../../../ui/resources/default_200_percent/close_2_hover.png') + 2x); +} + +#userList { + height: 166px; + padding: 5px; + width: 366px; +} + +#userList[disabled], +#userList[disabled] > [selected], +#userList[disabled] > :hover { + border-color: hsl(0, 0%, 85%); +} + +#userList[disabled] > [selected], +#userList[disabled] > :hover { + background-color: hsl(0, 0%, 90%); +} + +#userList[disabled] .remove-user-button { + visibility: hidden; +} + +#userNameEdit { + border: 1px solid lightgrey; + width: 366px; +} + +#ownerOnlyWarning { + -webkit-padding-start: 20px; + background-image: url('warning.png'); + background-repeat: no-repeat; + margin-bottom: 10px; + margin-top: 10px; + min-height: 17px; + padding-bottom: 1px; +} + +input#userNameEdit:invalid { + background-color: rgb(255, 102, 102); +} diff --git a/chromium/chrome/browser/resources/options/chromeos/accounts_user_list.js b/chromium/chrome/browser/resources/options/chromeos/accounts_user_list.js new file mode 100644 index 00000000000..419ab3b3709 --- /dev/null +++ b/chromium/chrome/browser/resources/options/chromeos/accounts_user_list.js @@ -0,0 +1,194 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options.accounts', function() { + /** @const */ var List = cr.ui.List; + /** @const */ var ListItem = cr.ui.ListItem; + /** @const */ var ArrayDataModel = cr.ui.ArrayDataModel; + + /** + * Creates a new user list. + * @param {Object=} opt_propertyBag Optional properties. + * @constructor + * @extends {cr.ui.List} + */ + var UserList = cr.ui.define('list'); + + UserList.prototype = { + __proto__: List.prototype, + + pref: 'cros.accounts.users', + + /** @override */ + decorate: function() { + List.prototype.decorate.call(this); + + // HACK(arv): http://crbug.com/40902 + window.addEventListener('resize', this.redraw.bind(this)); + + var self = this; + + // Listens to pref changes. + Preferences.getInstance().addEventListener(this.pref, + function(event) { + self.load_(event.value.value); + }); + }, + + createItem: function(user) { + return new UserListItem(user); + }, + + /** + * Finds the index of user by given username (canonicalized email). + * @private + * @param {string} username The username to look for. + * @return {number} The index of the found user or -1 if not found. + */ + indexOf_: function(username) { + var dataModel = this.dataModel; + if (!dataModel) + return -1; + + var length = dataModel.length; + for (var i = 0; i < length; ++i) { + var user = dataModel.item(i); + if (user.username == username) { + return i; + } + } + + return -1; + }, + + /** + * Update given user's account picture. + * @param {string} username User for which to update the image. + */ + updateAccountPicture: function(username) { + var index = this.indexOf_(username); + if (index >= 0) { + var item = this.getListItemByIndex(index); + if (item) + item.updatePicture(); + } + }, + + /** + * Loads given user list. + * @param {Array.<Object>} users An array of user info objects. + * @private + */ + load_: function(users) { + this.dataModel = new ArrayDataModel(users); + }, + + /** + * Removes given user from the list. + * @param {Object} user User info object to be removed from user list. + * @private + */ + removeUser_: function(user) { + var e = new Event('remove'); + e.user = user; + this.dispatchEvent(e); + } + }; + + /** + * Whether the user list is disabled. Only used for display purpose. + * @type {boolean} + */ + cr.defineProperty(UserList, 'disabled', cr.PropertyKind.BOOL_ATTR); + + /** + * Creates a new user list item. + * @param {Object} user The user account this represents. + * @constructor + * @extends {cr.ui.ListItem} + */ + function UserListItem(user) { + var el = cr.doc.createElement('div'); + el.user = user; + UserListItem.decorate(el); + return el; + } + + /** + * Decorates an element as a user account item. + * @param {!HTMLElement} el The element to decorate. + */ + UserListItem.decorate = function(el) { + el.__proto__ = UserListItem.prototype; + el.decorate(); + }; + + UserListItem.prototype = { + __proto__: ListItem.prototype, + + /** @override */ + decorate: function() { + ListItem.prototype.decorate.call(this); + + this.className = 'user-list-item'; + + this.icon_ = this.ownerDocument.createElement('img'); + this.icon_.className = 'user-icon'; + this.updatePicture(); + + var labelEmail = this.ownerDocument.createElement('span'); + labelEmail.className = 'user-email-label'; + labelEmail.textContent = this.user.email; + + var labelName = this.ownerDocument.createElement('span'); + labelName.className = 'user-name-label'; + labelName.textContent = this.user.owner ? + loadTimeData.getStringF('username_format', this.user.name) : + this.user.name; + + var emailNameBlock = this.ownerDocument.createElement('div'); + emailNameBlock.className = 'user-email-name-block'; + emailNameBlock.appendChild(labelEmail); + emailNameBlock.appendChild(labelName); + emailNameBlock.title = this.user.owner ? + loadTimeData.getStringF('username_format', this.user.email) : + this.user.email; + + this.appendChild(this.icon_); + this.appendChild(emailNameBlock); + + if (!this.user.owner) { + var removeButton = this.ownerDocument.createElement('button'); + removeButton.className = + 'raw-button remove-user-button custom-appearance'; + removeButton.addEventListener( + 'click', this.handleRemoveButtonClick_.bind(this)); + this.appendChild(removeButton); + } + }, + + /** + * Handles click on the remove button. + * @param {Event} e Click event. + * @private + */ + handleRemoveButtonClick_: function(e) { + // Handle left button click + if (e.button == 0) + this.parentNode.removeUser_(this.user); + }, + + /** + * Reloads user picture. + */ + updatePicture: function() { + this.icon_.src = 'chrome://userimage/' + this.user.username + + '?id=' + (new Date()).getTime(); + } + }; + + return { + UserList: UserList + }; +}); diff --git a/chromium/chrome/browser/resources/options/chromeos/accounts_user_name_edit.js b/chromium/chrome/browser/resources/options/chromeos/accounts_user_name_edit.js new file mode 100644 index 00000000000..88d2878d11f --- /dev/null +++ b/chromium/chrome/browser/resources/options/chromeos/accounts_user_name_edit.js @@ -0,0 +1,128 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options.accounts', function() { + /** + * Email alias only, assuming it's a gmail address. + * e.g. 'john' + * {name: 'john', email: 'john@gmail.com'} + * @const + */ + var format1String = + '^\\s*([\\w\\.!#\\$%&\'\\*\\+-\\/=\\?\\^`\\{\\|\\}~]+)\\s*$'; + /** + * Email address only. + * e.g. 'john@chromium.org' + * {name: 'john', email: 'john@chromium.org'} + * @const + */ + var format2String = + '^\\s*([\\w\\.!#\\$%&\'\\*\\+-\\/=\\?\\^`\\{\\|\\}~]+)@' + + '([A-Za-z0-9\-]{2,63}\\..+)\\s*$'; + /** + * Full format. + * e.g. '"John Doe" <john@chromium.org>' + * {name: 'John doe', email: 'john@chromium.org'} + * @const + */ + var format3String = + '^\\s*"{0,1}([^"]+)"{0,1}\\s*' + + '<([\\w\\.!#\\$%&\'\\*\\+-\\/=\\?\\^`\\{\\|\\}~]+@' + + '[A-Za-z0-9\-]{2,63}\\..+)>\\s*$'; + + /** + * Creates a new user name edit element. + * @param {Object=} opt_propertyBag Optional properties. + * @constructor + * @extends {HTMLInputElement} + */ + var UserNameEdit = cr.ui.define('input'); + + UserNameEdit.prototype = { + __proto__: HTMLInputElement.prototype, + + /** + * Called when an element is decorated as a user name edit. + */ + decorate: function() { + this.pattern = format1String + '|' + format2String + '|' + + format3String; + + this.onkeydown = this.handleKeyDown_.bind(this); + }, + + + /** + * Parses given str for user info. + * + * Note that the email parsing is based on RFC 5322 and does not support + * IMA (Internationalized Email Address). We take only the following chars + * as valid for an email alias (aka local-part): + * - Letters: a–z, A–Z + * - Digits: 0-9 + * - Characters: ! # $ % & ' * + - / = ? ^ _ ` { | } ~ + * - Dot: . (Note that we did not cover the cases that dot should not + * appear as first or last character and should not appear two or + * more times in a row.) + * + * @param {string} str A string to parse. + * @return {{name: string, email: string}} User info parsed from the string. + */ + parse: function(str) { + /** @const */ var format1 = new RegExp(format1String); + /** @const */ var format2 = new RegExp(format2String); + /** @const */ var format3 = new RegExp(format3String); + + var matches = format1.exec(str); + if (matches) { + return { + name: matches[1], + email: matches[1] + '@gmail.com' + }; + } + + matches = format2.exec(str); + if (matches) { + return { + name: matches[1], + email: matches[1] + '@' + matches[2] + }; + } + + matches = format3.exec(str); + if (matches) { + return { + name: matches[1], + email: matches[2] + }; + } + + return null; + }, + + /** + * Handler for key down event. + * @private + * @param {!Event} e The keydown event object. + */ + handleKeyDown_: function(e) { + if (e.keyIdentifier == 'Enter') { + var user = this.parse(this.value); + if (user) { + var event = new Event('add'); + event.user = user; + this.dispatchEvent(event); + } + this.select(); + // Avoid double-handling so the dialog doesn't close. + e.stopPropagation(); + } + } + }; + + return { + UserNameEdit: UserNameEdit + }; +}); + diff --git a/chromium/chrome/browser/resources/options/chromeos/bluetooth.css b/chromium/chrome/browser/resources/options/chromeos/bluetooth.css new file mode 100644 index 00000000000..e6a91e40705 --- /dev/null +++ b/chromium/chrome/browser/resources/options/chromeos/bluetooth.css @@ -0,0 +1,162 @@ +/* Copyright (c) 2012 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ + +.bluetooth-device-list { + margin: 10px 0; + padding: 5px 10px; +} + +.bluetooth-device[notconnectable] { + color: gray; +} + +.bluetooth-device[connected] { + font-weight: bold; /* semibold */ +} + +#bluetooth-options .bluetooth-device-list { + margin: 0 10px; +} + +#bluetooth-options .button-strip { + -webkit-box-pack: justify; +} + +#bluetooth-options .button-strip #bluetooth-scanning-label { + -webkit-box-flex: 1; + display: block; +} + +#bluetooth-scanning-label { + -webkit-margin-start: 5px; + color: #999; +} + +#bluetooth-scanning-icon { + height: 20px; + opacity: 0.66; + vertical-align: middle; + width: 20px; +} + +#bluetooth-paired-devices-list { + min-height: 96px !important; +} + +#bluetooth-paired-devices-list, +#bluetooth-unpaired-devices-list { + /* Prevent dialog from expanding if many devices are found. */ + max-height: 192px; + overflow-x: hidden; + overflow-y: auto; +} + +.bluetooth-empty-list-label { + box-sizing: border-box; + color: #999; + padding-top: 32px; + text-align: center; +} + +#bluetooth-paired-devices-list-empty-placeholder { + height: 96px; +} + +#bluetooth-unpaired-devices-list-empty-placeholder { + height: 192px; +} + +/* Fix the dimensions of the message area so that the dialog does not change + change size during the pairing process as the message changes. Sized + generously to accomodate the longest of the messages. */ +#bluetooth-pairing-message-area { + display: table; + height: 160px; + padding: 6px 0 !important; + width: 420px; +} + +/* Force the message to be vertical centered so that a shorter message does not + look out of place when there is room for a much longer message. */ +#bluetooth-pairing-message-contents { + display: table-cell; + vertical-align: middle; +} + +#bluetooth-pairing-instructions, +#bluetooth-pairing-passkey-display, +#bluetooth-pairing-passkey-entry, +#bluetooth-pairing-pincode-entry, +#bluetooth-passkey, +#bluetooth-pincode { + text-align: center; +} + +#bluetooth-pairing-instructions { + margin: 10px; +} + +#bluetooth-pairing-passkey-display, +#bluetooth-pairing-passkey-entry, +#bluetooth-pairing-pincode-entry { + margin: 40px 0; +} + +.bluetooth-keyboard-button { + -webkit-padding-end: 15px; + -webkit-padding-start: 15px; + background-image: -webkit-gradient(linear, + left top, + left bottom, + color-stop(0, #e9e9e9), + color-stop(1, #f5f5f5)); + border: 1px solid #d4d4d4; + border-radius: 4px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.07), + inset 0 1px 1px 1px #fff, + inset 0 -1px 1px 1px #ddd; + color: #666; + display: inline-block; + font-size: 14px; + font-weight: 600; + height: 38px; + line-height: 38px; + margin: 0 10px 0 0; + position: relative; + text-align: center; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + vertical-align: baseline; +} + +.bluetooth-keyboard-button:last-child { + margin: 0; +} + +#bluetooth-enter-key { + min-width: 54px; +} + +.bluetooth-passkey-char { + -webkit-margin-start: 45px; + color: #999; + font-size: 20px; + font-weight: 600; /* semibold */ + padding-bottom: 5px; +} + +.bluetooth-passkey-char:first-child { + -webkit-margin-start: 0; +} + +.bluetooth-keyboard-button.key-typed { + border: 1px solid #ccc; + box-shadow: 0 0 0 1px #888, + inset 0 1px 1px 1px #fff, + inset 0 -1px 1px 1px #eee; + color: #222; +} + +.bluetooth-keyboard-button.key-pin { + color: #222; +} diff --git a/chromium/chrome/browser/resources/options/chromeos/bluetooth_add_device_overlay.html b/chromium/chrome/browser/resources/options/chromeos/bluetooth_add_device_overlay.html new file mode 100644 index 00000000000..534d9634d30 --- /dev/null +++ b/chromium/chrome/browser/resources/options/chromeos/bluetooth_add_device_overlay.html @@ -0,0 +1,23 @@ +<div id="bluetooth-options" class="page" hidden> + <div class="close-button"></div> + <h1 i18n-content="bluetoothAddDeviceTitle"></h1> + <div class="settings-list bluetooth-device-list content-area"> + <list id="bluetooth-unpaired-devices-list"></list> + <div id="bluetooth-unpaired-devices-list-empty-placeholder" + class="bluetooth-empty-list-label" hidden> + <span i18n-content="bluetoothNoDevicesFound"></span> + </div> + </div> + <div class="action-area button-strip"> + <button id="bluetooth-add-device-cancel-button" type="reset" + i18n-content="cancel"> + </button> + <button id="bluetooth-add-device-apply-button" type="submit" + class="default-button" i18n-content="bluetoothConnectDevice" disabled> + </button> + <span id="bluetooth-scanning-label" + i18n-content="bluetoothScanning"> + </span> + <div id="bluetooth-scanning-icon" class="inline-spinner"></div> + </div> +</div> diff --git a/chromium/chrome/browser/resources/options/chromeos/bluetooth_add_device_overlay.js b/chromium/chrome/browser/resources/options/chromeos/bluetooth_add_device_overlay.js new file mode 100644 index 00000000000..3b076a1f26f --- /dev/null +++ b/chromium/chrome/browser/resources/options/chromeos/bluetooth_add_device_overlay.js @@ -0,0 +1,95 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + /** @const */ var OptionsPage = options.OptionsPage; + + /** + * Encapsulated handling of the Bluetooth options page. + * @constructor + */ + function BluetoothOptions() { + OptionsPage.call(this, + 'bluetooth', + loadTimeData.getString('bluetoothOptionsPageTabTitle'), + 'bluetooth-options'); + } + + cr.addSingletonGetter(BluetoothOptions); + + BluetoothOptions.prototype = { + __proto__: OptionsPage.prototype, + + /** + * The list of available (unpaired) bluetooth devices. + * @type {DeletableItemList} + * @private + */ + deviceList_: null, + + /** @override */ + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + this.createDeviceList_(); + + $('bluetooth-add-device-cancel-button').onclick = function(event) { + chrome.send('stopBluetoothDeviceDiscovery'); + OptionsPage.closeOverlay(); + }; + + var self = this; + $('bluetooth-add-device-apply-button').onclick = function(event) { + var device = self.deviceList_.selectedItem; + var address = device.address; + chrome.send('stopBluetoothDeviceDiscovery'); + OptionsPage.closeOverlay(); + device.pairing = 'bluetoothStartConnecting'; + options.BluetoothPairing.showDialog(device); + chrome.send('updateBluetoothDevice', [address, 'connect']); + }; + + $('bluetooth-unpaired-devices-list').addEventListener('change', + function() { + var item = $('bluetooth-unpaired-devices-list').selectedItem; + // The "bluetooth-add-device-apply-button" should be enabled for devices + // that can be paired or remembered. Devices not supporting pairing will + // be just remembered and later reported as "item.paired" = true. The + // button should be disabled in any other case: + // * No item is selected (item is undefined). + // * Paired devices (item.paired is true) are already paired and a new + // pairing attempt will fail. Paired devices could appear in this list + // shortly after the pairing initiated in another window finishes. + // * "Connecting" devices (item.connecting is true) are in the process + // of a pairing or connection. Another attempt to pair before the + // ongoing pair finishes will fail, so the button should be disabled. + var disabled = !item || item.paired || item.connecting; + $('bluetooth-add-device-apply-button').disabled = disabled; + }); + }, + + /** + * Creates, decorates and initializes the bluetooth device list. + * @private + */ + createDeviceList_: function() { + this.deviceList_ = $('bluetooth-unpaired-devices-list'); + options.system.bluetooth.BluetoothDeviceList.decorate(this.deviceList_); + } + }; + + /** + * Automatically start the device discovery process if the + * "Add device" dialog is visible. + */ + BluetoothOptions.updateDiscovery = function() { + var page = BluetoothOptions.getInstance(); + if (page && page.visible) + chrome.send('findBluetoothDevices'); + } + + // Export + return { + BluetoothOptions: BluetoothOptions + }; +}); diff --git a/chromium/chrome/browser/resources/options/chromeos/bluetooth_device_list.js b/chromium/chrome/browser/resources/options/chromeos/bluetooth_device_list.js new file mode 100644 index 00000000000..ede390fe4db --- /dev/null +++ b/chromium/chrome/browser/resources/options/chromeos/bluetooth_device_list.js @@ -0,0 +1,347 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options.system.bluetooth', function() { + /** @const */ var ArrayDataModel = cr.ui.ArrayDataModel; + /** @const */ var DeletableItem = options.DeletableItem; + /** @const */ var DeletableItemList = options.DeletableItemList; + /** @const */ var ListSingleSelectionModel = cr.ui.ListSingleSelectionModel; + + /** + * Bluetooth settings constants. + */ + function Constants() {} + + /** + * Creates a new bluetooth list item. + * @param {{name: string, + * address: string, + * paired: boolean, + * connected: boolean, + * connecting: boolean, + * connectable: boolean, + * pairing: string|undefined, + * passkey: number|undefined, + * pincode: string|undefined, + * entered: number|undefined}} device + * Description of the Bluetooth device. + * @constructor + * @extends {options.DeletableItem} + */ + function BluetoothListItem(device) { + var el = cr.doc.createElement('div'); + el.__proto__ = BluetoothListItem.prototype; + el.data = {}; + for (var key in device) + el.data[key] = device[key]; + el.decorate(); + // Only show the close button for paired devices, but not for connecting + // devices. + el.deletable = device.paired && !device.connecting; + return el; + } + + BluetoothListItem.prototype = { + __proto__: DeletableItem.prototype, + + /** + * Description of the Bluetooth device. + * @type {{name: string, + * address: string, + * paired: boolean, + * connected: boolean, + * connecting: boolean, + * connectable: boolean, + * pairing: string|undefined, + * passkey: number|undefined, + * pincode: string|undefined, + * entered: number|undefined}} + */ + data: null, + + /** @override */ + decorate: function() { + DeletableItem.prototype.decorate.call(this); + var label = this.ownerDocument.createElement('div'); + label.className = 'bluetooth-device-label'; + this.classList.add('bluetooth-device'); + // There are four kinds of devices we want to distinguish: + // * Connecting devices: in bold with a "connecting" label, + // * Connected devices: in bold, + // * Paired, not connected but connectable devices: regular and + // * Paired, not connected and not connectable devices: grayed out. + this.connected = this.data.connecting || + (this.data.paired && this.data.connected); + this.notconnectable = this.data.paired && !this.data.connecting && + !this.data.connected && !this.data.connectable; + // "paired" devices are those that are remembered but not connected. + this.paired = this.data.paired && !this.data.connected && + this.data.connectable; + + var content = this.data.name; + // Update the device's label according to its state. A "connecting" device + // can be in the process of connecting and pairing, so we check connecting + // first. + if (this.data.connecting) { + content = loadTimeData.getStringF('bluetoothDeviceConnecting', + this.data.name); + } + label.textContent = content; + this.contentElement.appendChild(label); + }, + }; + + /** + * Class for displaying a list of Bluetooth devices. + * @constructor + * @extends {options.DeletableItemList} + */ + var BluetoothDeviceList = cr.ui.define('list'); + + BluetoothDeviceList.prototype = { + __proto__: DeletableItemList.prototype, + + /** + * Height of a list entry in px. + * @type {number} + * @private + */ + itemHeight_: 32, + + /** + * Width of a list entry in px. + * @type {number} + * @private. + */ + itemWidth_: 400, + + /** @override */ + decorate: function() { + DeletableItemList.prototype.decorate.call(this); + // Force layout of all items even if not in the viewport to address + // errors in scroll positioning when the list is hidden during initial + // layout. The impact on performance should be minimal given that the + // list is not expected to grow very large. Fixed height items are also + // required to avoid caching incorrect sizes during layout of a hidden + // list. + this.autoExpands = true; + this.fixedHeight = true; + this.clear(); + this.selectionModel = new ListSingleSelectionModel(); + }, + + /** + * Adds a bluetooth device to the list of available devices. A check is + * made to see if the device is already in the list, in which case the + * existing device is updated. + * @param {{name: string, + * address: string, + * paired: boolean, + * connected: boolean, + * connecting: boolean, + * connectable: boolean, + * pairing: string|undefined, + * passkey: number|undefined, + * pincode: string|undefined, + * entered: number|undefined}} device + * Description of the bluetooth device. + * @return {boolean} True if the devies was successfully added or updated. + */ + appendDevice: function(device) { + var selectedDevice = this.getSelectedDevice_(); + var index = this.find(device.address); + if (index == undefined) { + this.dataModel.push(device); + this.redraw(); + } else { + this.dataModel.splice(index, 1, device); + this.redrawItem(index); + } + this.updateListVisibility_(); + if (selectedDevice) + this.setSelectedDevice_(selectedDevice); + return true; + }, + + /** + * Forces a revailidation of the list content. Deleting a single item from + * the list results in a stale cache requiring an invalidation. + * @param {string=} opt_selection Optional address of device to select + * after refreshing the list. + */ + refresh: function(opt_selection) { + // TODO(kevers): Investigate if the stale cache issue can be fixed in + // cr.ui.list. + var selectedDevice = opt_selection ? opt_selection : + this.getSelectedDevice_(); + this.invalidate(); + this.redraw(); + if (selectedDevice) + this.setSelectedDevice_(selectedDevice); + }, + + /** + * Retrieves the address of the selected device, or null if no device is + * selected. + * @return {?string} Address of selected device or null. + * @private + */ + getSelectedDevice_: function() { + var selection = this.selectedItem; + if (selection) + return selection.address; + return null; + }, + + /** + * Selects the device with the matching address. + * @param {string} address The unique address of the device. + * @private + */ + setSelectedDevice_: function(address) { + var index = this.find(address); + if (index != undefined) + this.selectionModel.selectRange(index, index); + }, + + /** + * Perges all devices from the list. + */ + clear: function() { + this.dataModel = new ArrayDataModel([]); + this.redraw(); + this.updateListVisibility_(); + }, + + /** + * Returns the index of the list entry with the matching address. + * @param {string} address Unique address of the Bluetooth device. + * @return {number|undefined} Index of the matching entry or + * undefined if no match found. + */ + find: function(address) { + var size = this.dataModel.length; + for (var i = 0; i < size; i++) { + var entry = this.dataModel.item(i); + if (entry.address == address) + return i; + } + }, + + /** @override */ + createItem: function(entry) { + return new BluetoothListItem(entry); + }, + + /** + * Overrides the default implementation, which is used to compute the + * size of an element in the list. The default implementation relies + * on adding a placeholder item to the list and fetching its size and + * position. This strategy does not work if an item is added to the list + * while it is hidden, as the computed metrics will all be zero in that + * case. + * @return {{height: number, marginTop: number, marginBottom: number, + * width: number, marginLeft: number, marginRight: number}} + * The height and width of the item, taking margins into account, + * and the margins themselves. + */ + measureItem: function() { + return { + height: this.itemHeight_, + marginTop: 0, + marginBotton: 0, + width: this.itemWidth_, + marginLeft: 0, + marginRight: 0 + }; + }, + + /** + * Override the default implementation to return a predetermined size, + * which in turns allows proper layout of items even if the list is hidden. + * @return {height: number, width: number} Dimensions of a single item in + * the list of bluetooth device. + * @private. + */ + getDefaultItemSize_: function() { + return { + height: this.itemHeight_, + width: this.itemWidth_ + }; + }, + + /** + * Override base implementation of handleClick_, which unconditionally + * removes the item. In this case, removal of the element is deferred + * pending confirmation from the Bluetooth adapter. + * @param {Event} e The click event object. + * @private + */ + handleClick_: function(e) { + if (this.disabled) + return; + + var target = e.target; + if (!target.classList.contains('row-delete-button')) + return; + + var item = this.getListItemAncestor(target); + var selected = this.selectionModel.selectedIndex; + var index = this.getIndexOfListItem(item); + if (item && item.deletable) { + if (selected != index) + this.setSelectedDevice_(item.data.address); + // Device is busy until we hear back from the Bluetooth adapter. + // Prevent double removal request. + item.deletable = false; + // TODO(kevers): Provide visual feedback that the device is busy. + + // Inform the bluetooth adapter that we are disconnecting or + // forgetting the device. + chrome.send('updateBluetoothDevice', + [item.data.address, item.connected ? 'disconnect' : 'forget']); + } + }, + + /** @override */ + deleteItemAtIndex: function(index) { + var selectedDevice = this.getSelectedDevice_(); + this.dataModel.splice(index, 1); + this.refresh(selectedDevice); + this.updateListVisibility_(); + }, + + /** + * If the list has an associated empty list placholder then update the + * visibility of the list and placeholder. + * @private + */ + updateListVisibility_: function() { + var empty = this.dataModel.length == 0; + var listPlaceHolderID = this.id + '-empty-placeholder'; + if ($(listPlaceHolderID)) { + if (this.hidden != empty) { + this.hidden = empty; + $(listPlaceHolderID).hidden = !empty; + this.refresh(); + } + } + }, + }; + + cr.defineProperty(BluetoothListItem, 'connected', cr.PropertyKind.BOOL_ATTR); + + cr.defineProperty(BluetoothListItem, 'paired', cr.PropertyKind.BOOL_ATTR); + + cr.defineProperty(BluetoothListItem, 'connecting', cr.PropertyKind.BOOL_ATTR); + + cr.defineProperty(BluetoothListItem, 'notconnectable', + cr.PropertyKind.BOOL_ATTR); + + return { + BluetoothListItem: BluetoothListItem, + BluetoothDeviceList: BluetoothDeviceList, + Constants: Constants + }; +}); diff --git a/chromium/chrome/browser/resources/options/chromeos/bluetooth_pair_device_overlay.html b/chromium/chrome/browser/resources/options/chromeos/bluetooth_pair_device_overlay.html new file mode 100644 index 00000000000..95b9c0339d5 --- /dev/null +++ b/chromium/chrome/browser/resources/options/chromeos/bluetooth_pair_device_overlay.html @@ -0,0 +1,28 @@ +<div id="bluetooth-pairing" class="page" hidden> + <div class="close-button"></div> + <h1 i18n-content="bluetoothAddDeviceTitle"></h1> + <div id="bluetooth-pairing-message-area" class="content-area"> + <div id="bluetooth-pairing-message-contents"> + <div id="bluetooth-pairing-instructions"></div> + <div id="bluetooth-pairing-passkey-display" hidden></div> + <div id="bluetooth-pairing-passkey-entry" hidden> + <input id="bluetooth-passkey" maxlength="6" type="text"> + </div> + <div id="bluetooth-pairing-pincode-entry" hidden> + <input id="bluetooth-pincode" maxlength="16" type="text"> + </div> + </div> + </div> + <div class="action-area button-strip"> + <button id="bluetooth-pair-device-cancel-button" type="reset" + i18n-content="cancel" hidden></button> + <button id="bluetooth-pair-device-connect-button" type="reset" + i18n-content="bluetoothConnectDevice" hidden></button> + <button id="bluetooth-pair-device-reject-button" type="reset" + i18n-content="bluetoothRejectPasskey" hidden></button> + <button id="bluetooth-pair-device-accept-button" type="reset" + i18n-content="bluetoothAcceptPasskey" hidden></button> + <button id="bluetooth-pair-device-dismiss-button" type="reset" + i18n-content="bluetoothDismissError" hidden></button> + </div> +</div> diff --git a/chromium/chrome/browser/resources/options/chromeos/bluetooth_pair_device_overlay.js b/chromium/chrome/browser/resources/options/chromeos/bluetooth_pair_device_overlay.js new file mode 100644 index 00000000000..a206e8bf72a --- /dev/null +++ b/chromium/chrome/browser/resources/options/chromeos/bluetooth_pair_device_overlay.js @@ -0,0 +1,398 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + /** @const */ var OptionsPage = options.OptionsPage; + + /** + * Enumeration of possible states during pairing. The value associated with + * each state maps to a localized string in the global variable + * |loadTimeData|. + * @enum {string} + */ + var PAIRING = { + STARTUP: 'bluetoothStartConnecting', + ENTER_PIN_CODE: 'bluetoothEnterPinCode', + ENTER_PASSKEY: 'bluetoothEnterPasskey', + REMOTE_PIN_CODE: 'bluetoothRemotePinCode', + REMOTE_PASSKEY: 'bluetoothRemotePasskey', + CONFIRM_PASSKEY: 'bluetoothConfirmPasskey', + CONNECT_FAILED: 'bluetoothConnectFailed', + CANCELED: 'bluetoothPairingCanceled', + DISMISSED: 'bluetoothPairingDismissed', // pairing dismissed(succeeded or + // canceled). + }; + + /** + * List of IDs for conditionally visible elements in the dialog. + * @type {Array.<string>} + * @const + */ + var ELEMENTS = ['bluetooth-pairing-passkey-display', + 'bluetooth-pairing-passkey-entry', + 'bluetooth-pairing-pincode-entry', + 'bluetooth-pair-device-connect-button', + 'bluetooth-pair-device-cancel-button', + 'bluetooth-pair-device-accept-button', + 'bluetooth-pair-device-reject-button', + 'bluetooth-pair-device-dismiss-button']; + + /** + * Encapsulated handling of the Bluetooth device pairing page. + * @constructor + */ + function BluetoothPairing() { + OptionsPage.call(this, + 'bluetoothPairing', + loadTimeData.getString('bluetoothOptionsPageTabTitle'), + 'bluetooth-pairing'); + } + + cr.addSingletonGetter(BluetoothPairing); + + BluetoothPairing.prototype = { + __proto__: OptionsPage.prototype, + + /** + * Description of the bluetooth device. + * @type {{name: string, + * address: string, + * paired: boolean, + * connected: boolean, + * connecting: boolean, + * connectable: boolean, + * pairing: string|undefined, + * passkey: number|undefined, + * pincode: string|undefined, + * entered: number|undefined}} + * @private. + */ + device_: null, + + /** + * Can the dialog be programmatically dismissed. + * @type {boolean} + */ + dismissible_: true, + + /** @override */ + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + var self = this; + $('bluetooth-pair-device-cancel-button').onclick = function() { + OptionsPage.closeOverlay(); + }; + $('bluetooth-pair-device-reject-button').onclick = function() { + chrome.send('updateBluetoothDevice', + [self.device_.address, 'reject']); + OptionsPage.closeOverlay(); + }; + $('bluetooth-pair-device-connect-button').onclick = function() { + var args = [self.device_.address, 'connect']; + var passkey = self.device_.passkey; + if (passkey) + args.push(String(passkey)); + else if (!$('bluetooth-pairing-passkey-entry').hidden) + args.push($('bluetooth-passkey').value); + else if (!$('bluetooth-pairing-pincode-entry').hidden) + args.push($('bluetooth-pincode').value); + chrome.send('updateBluetoothDevice', args); + // Prevent sending a 'connect' command twice. + $('bluetooth-pair-device-connect-button').disabled = true; + }; + $('bluetooth-pair-device-accept-button').onclick = function() { + chrome.send('updateBluetoothDevice', + [self.device_.address, 'accept']); + OptionsPage.closeOverlay(); + }; + $('bluetooth-pair-device-dismiss-button').onclick = function() { + OptionsPage.closeOverlay(); + }; + $('bluetooth-passkey').oninput = function() { + var inputField = $('bluetooth-passkey'); + var value = inputField.value; + // Note that using <input type="number"> is insufficient to restrict + // the input as it allows negative numbers and does not limit the + // number of charactes typed even if a range is set. Furthermore, + // it sometimes produces strange repaint artifacts. + var filtered = value.replace(/[^0-9]/g, ''); + if (filtered != value) + inputField.value = filtered; + $('bluetooth-pair-device-connect-button').disabled = + inputField.value.length == 0; + } + $('bluetooth-pincode').oninput = function() { + $('bluetooth-pair-device-connect-button').disabled = + $('bluetooth-pincode').value.length == 0; + } + $('bluetooth-passkey').addEventListener('keydown', + this.keyDownEventHandler_.bind(this)); + $('bluetooth-pincode').addEventListener('keydown', + this.keyDownEventHandler_.bind(this)); + }, + + /** @override */ + didClosePage: function() { + if (this.device_.pairing != PAIRING.DISMISSED && + this.device_.pairing != PAIRING.CONNECT_FAILED) { + this.device_.pairing = PAIRING.CANCELED; + chrome.send('updateBluetoothDevice', + [this.device_.address, 'cancel']); + } + }, + + /** + * Override to prevent showing the overlay if the Bluetooth device details + * have not been specified. Prevents showing an empty dialog if the user + * quits and restarts Chrome while in the process of pairing with a device. + * @return {boolean} True if the overlay can be displayed. + */ + canShowPage: function() { + return this.device_ && this.device_.address && this.device_.pairing; + }, + + /** + * Sets input focus on the passkey or pincode field if appropriate. + */ + didShowPage: function() { + if (!$('bluetooth-pincode').hidden) + $('bluetooth-pincode').focus(); + else if (!$('bluetooth-passkey').hidden) + $('bluetooth-passkey').focus(); + }, + + /** + * Configures the overlay for pairing a device. + * @param {Object} device Description of the bluetooth device. + */ + update: function(device) { + this.device_ = {}; + for (key in device) + this.device_[key] = device[key]; + // Update the pairing instructions. + var instructionsEl = $('bluetooth-pairing-instructions'); + this.clearElement_(instructionsEl); + this.dismissible_ = ('dismissible' in device) ? + device.dismissible : true; + + var message = loadTimeData.getString(device.pairing); + message = message.replace('%1', this.device_.name); + instructionsEl.textContent = message; + + // Update visibility of dialog elements. + if (this.device_.passkey) { + this.updatePasskey_(); + if (this.device_.pairing == PAIRING.CONFIRM_PASSKEY) { + // Confirming a match between displayed passkeys. + this.displayElements_(['bluetooth-pairing-passkey-display', + 'bluetooth-pair-device-accept-button', + 'bluetooth-pair-device-reject-button']); + } else { + // Remote entering a passkey. + this.displayElements_(['bluetooth-pairing-passkey-display', + 'bluetooth-pair-device-cancel-button']); + } + } else if (this.device_.pincode) { + this.updatePinCode_(); + this.displayElements_(['bluetooth-pairing-passkey-display', + 'bluetooth-pair-device-cancel-button']); + } else if (this.device_.pairing == PAIRING.ENTER_PIN_CODE) { + // Prompting the user to enter a PIN code. + this.displayElements_(['bluetooth-pairing-pincode-entry', + 'bluetooth-pair-device-connect-button', + 'bluetooth-pair-device-cancel-button']); + $('bluetooth-pincode').value = ''; + } else if (this.device_.pairing == PAIRING.ENTER_PASSKEY) { + // Prompting the user to enter a passkey. + this.displayElements_(['bluetooth-pairing-passkey-entry', + 'bluetooth-pair-device-connect-button', + 'bluetooth-pair-device-cancel-button']); + $('bluetooth-passkey').value = ''; + } else if (this.device_.pairing == PAIRING.STARTUP) { + // Starting the pairing process. + this.displayElements_(['bluetooth-pair-device-cancel-button']); + } else { + // Displaying an error message. + this.displayElements_(['bluetooth-pair-device-dismiss-button']); + } + // User is required to enter a passkey or pincode before the connect + // button can be enabled. The 'oninput' methods for the input fields + // determine when the connect button becomes active. + $('bluetooth-pair-device-connect-button').disabled = true; + }, + + /** + * Handles the ENTER key for the passkey or pincode entry field. + * @return {Event} a keydown event. + * @private + */ + keyDownEventHandler_: function(event) { + /** @const */ var ENTER_KEY_CODE = 13; + if (event.keyCode == ENTER_KEY_CODE) { + var button = $('bluetooth-pair-device-connect-button'); + if (!button.hidden) + button.click(); + } + }, + + /** + * Updates the visibility of elements in the dialog. + * @param {Array.<string>} list List of conditionally visible elements that + * are to be made visible. + * @private + */ + displayElements_: function(list) { + var enabled = {}; + for (var i = 0; i < list.length; i++) { + var key = list[i]; + enabled[key] = true; + } + for (var i = 0; i < ELEMENTS.length; i++) { + var key = ELEMENTS[i]; + $(key).hidden = !enabled[key]; + } + }, + + /** + * Removes all children from an element. + * @param {!Element} element Target element to clear. + */ + clearElement_: function(element) { + var child = element.firstChild; + while (child) { + element.removeChild(child); + child = element.firstChild; + } + }, + + /** + * Formats an element for displaying the passkey. + */ + updatePasskey_: function() { + var passkeyEl = $('bluetooth-pairing-passkey-display'); + var keyClass = this.device_.pairing == PAIRING.REMOTE_PASSKEY ? + 'bluetooth-keyboard-button' : 'bluetooth-passkey-char'; + this.clearElement_(passkeyEl); + var key = String(this.device_.passkey); + // Passkey should always have 6 digits. + key = '000000'.substring(0, 6 - key.length) + key; + var progress = this.device_.entered; + for (var i = 0; i < key.length; i++) { + var keyEl = document.createElement('span'); + keyEl.textContent = key.charAt(i); + keyEl.className = keyClass; + if (progress == undefined) + keyEl.classList.add('key-pin'); + else if (i < progress) + keyEl.classList.add('key-typed'); + passkeyEl.appendChild(keyEl); + } + if (this.device_.pairing == PAIRING.REMOTE_PASSKEY) { + // Add enter key. + var label = loadTimeData.getString('bluetoothEnterKey'); + var keyEl = document.createElement('span'); + keyEl.textContent = label; + keyEl.className = keyClass; + keyEl.id = 'bluetooth-enter-key'; + if (progress == undefined) + keyEl.classList.add('key-pin'); + else if (progress > key.length) + keyEl.classList.add('key-typed'); + passkeyEl.appendChild(keyEl); + } + passkeyEl.hidden = false; + }, + + /** + * Formats an element for displaying the PIN code. + */ + updatePinCode_: function() { + var passkeyEl = $('bluetooth-pairing-passkey-display'); + var keyClass = this.device_.pairing == PAIRING.REMOTE_PIN_CODE ? + 'bluetooth-keyboard-button' : 'bluetooth-passkey-char'; + this.clearElement_(passkeyEl); + var key = String(this.device_.pincode); + for (var i = 0; i < key.length; i++) { + var keyEl = document.createElement('span'); + keyEl.textContent = key.charAt(i); + keyEl.className = keyClass; + keyEl.classList.add('key-pin'); + passkeyEl.appendChild(keyEl); + } + if (this.device_.pairing == PAIRING.REMOTE_PIN_CODE) { + // Add enter key. + var label = loadTimeData.getString('bluetoothEnterKey'); + var keyEl = document.createElement('span'); + keyEl.textContent = label; + keyEl.className = keyClass; + keyEl.classList.add('key-pin'); + keyEl.id = 'bluetooth-enter-key'; + passkeyEl.appendChild(keyEl); + } + passkeyEl.hidden = false; + }, + }; + + /** + * Configures the device pairing instructions and displays the pairing + * overlay. + * @param {Object} device Description of the Bluetooth device. + */ + BluetoothPairing.showDialog = function(device) { + BluetoothPairing.getInstance().update(device); + OptionsPage.showPageByName('bluetoothPairing', false); + }; + + /** + * Displays a message from the Bluetooth adapter. + * @param {{string: label, + * string: address} data Data for constructing the message. + */ + BluetoothPairing.showMessage = function(data) { + var name = data.address; + if (name.length == 0) + return; + var dialog = BluetoothPairing.getInstance(); + if (dialog.device_ && name == dialog.device_.address && + dialog.device_.pairing == PAIRING.CANCELED) { + // Do not show any error message after cancelation of the pairing. + return; + } + + var list = $('bluetooth-paired-devices-list'); + if (list) { + var index = list.find(name); + if (index == undefined) { + list = $('bluetooth-unpaired-devices-list'); + index = list.find(name); + } + if (index != undefined) { + var entry = list.dataModel.item(index); + if (entry && entry.name) + name = entry.name; + } + } + BluetoothPairing.showDialog({name: name, + address: data.address, + pairing: data.label, + dismissible: false}); + }; + + /** + * Closes the Bluetooth pairing dialog. + */ + BluetoothPairing.dismissDialog = function() { + var overlay = OptionsPage.getTopmostVisiblePage(); + var dialog = BluetoothPairing.getInstance(); + if (overlay == dialog && dialog.dismissible_) { + dialog.device_.pairing = PAIRING.DISMISSED; + OptionsPage.closeOverlay(); + } + }; + + // Export + return { + BluetoothPairing: BluetoothPairing + }; +}); diff --git a/chromium/chrome/browser/resources/options/chromeos/browser_options.css b/chromium/chrome/browser/resources/options/chromeos/browser_options.css new file mode 100644 index 00000000000..e02cae24c7d --- /dev/null +++ b/chromium/chrome/browser/resources/options/chromeos/browser_options.css @@ -0,0 +1,7 @@ +/* Copyright 2013 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ + +#sync-section { + min-height: 64px; +} diff --git a/chromium/chrome/browser/resources/options/chromeos/change_picture_options.css b/chromium/chrome/browser/resources/options/chromeos/change_picture_options.css new file mode 100644 index 00000000000..ab0d497df17 --- /dev/null +++ b/chromium/chrome/browser/resources/options/chromeos/change_picture_options.css @@ -0,0 +1,195 @@ +/* Copyright (c) 2012 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ + +#user-images-area { + display: -webkit-box; +} + +#user-image-grid { + -webkit-user-drag: none; + -webkit-user-select: none; + height: 264px; + margin: 10px; + outline: none; + /* Necessary for correct metrics calculation by grid.js. */ + overflow: hidden; + padding: 0; + width: 530px; +} + +#user-image-grid * { + margin: 0; + padding: 0; +} + +#user-image-grid img { + background-color: white; + height: 64px; + vertical-align: middle; + width: 64px; +} + +#user-image-grid > li { + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 4px; + display: inline-block; + margin: 8px; + padding: 3px; +} + +#user-image-grid [selected] { + border: 2px solid rgb(0, 102, 204); + padding: 2px; +} + +/** + * #user-image-preview can have the following classes: + * .default-image: one of the default images is selected (including the grey + * silhouette); + * .profile-image: profile image is selected; + * .online: camera is streaming video; + * .camera: camera (live or photo) is selected; + * .live: camera is in live mode (no photo taken yet/last photo removed). + */ + +#user-image-preview { + margin: 20px 10px 0 0; + max-width: 220px; + position: relative; +} + +#user-image-preview .perspective-box { + -webkit-perspective: 600px; +} + +#user-image-preview-img { + background: white; + border: solid 1px #cacaca; + border-radius: 4px; + max-height: 220px; + max-width: 220px; + padding: 2px; +} + +.camera.live #user-image-preview-img { + display: none; +} + +.animation #user-image-preview-img { + -webkit-transition: -webkit-transform 200ms linear; +} + +.camera.flip-x #user-image-preview-img { + -webkit-transform: rotateY(180deg); +} + +.user-image-stream-area { + display: none; + position: relative; +} + +.camera.live .user-image-stream-area { + display: block; +} + +#user-image-stream-crop { + /* TODO(ivankr): temporary workaround for crbug.com/142347. */ + -webkit-transform: rotateY(360deg); + -webkit-transition: -webkit-transform 200ms linear; + height: 220px; + overflow: hidden; + position: relative; + width: 220px; +} + +.flip-x #user-image-stream-crop { + -webkit-transform: rotateY(180deg); +} + +/* TODO(ivankr): specify dimensions from real capture size. */ +.user-image-stream { + border: solid 1px #cacaca; + height: 220px; + /* Center image for 4:3 aspect ratio. */ + left: -16.6%; + position: absolute; + visibility: hidden; +} + +.online .user-image-stream { + visibility: visible; +} + +.user-image-stream-area .spinner { + display: none; + height: 44px; + left: 50%; + margin: -22px 0 0 -22px; + position: absolute; + top: 50%; + width: 44px; +} + +.camera.live:not(.online) .user-image-stream-area .spinner { + display: block; +} + +#flip-photo { + -webkit-transition: opacity 75ms linear; + background: url('chrome://theme/IDR_MIRROR_FLIP') no-repeat; + border: none; + bottom: 44px; /* 8px + image bottom. */ + display: block; + height: 32px; + opacity: 0; + position: absolute; + right: 8px; + width: 32px; +} + +/* TODO(merkulova): remove when webkit crbug.com/126479 is fixed. */ +.flip-trick { + -webkit-transform: translateZ(1px); +} + +html[dir=rtl] #flip-photo { + left: 8px; + right: auto; +} + +/* "Flip photo" button is hidden during flip animation. */ +.camera.online:not(.animation) #flip-photo, +.camera:not(.live):not(.animation) #flip-photo { + opacity: 0.75; +} + +#discard-photo, +#take-photo { + display: none; + height: 25px; + margin: 4px 1px; + padding: 0; + width: 220px; +} + +.camera:not(.live) #discard-photo { + background: url('chrome://theme/IDR_USER_IMAGE_RECYCLE') + no-repeat center 0; + display: block; +} + +.camera.live.online #take-photo { + background: url('chrome://theme/IDR_USER_IMAGE_CAPTURE') + no-repeat center -1px; + display: block; +} + +#user-image-attribution { + -webkit-padding-start: 34px; + line-height: 26px; +} + +#user-image-author-website { + -webkit-padding-start: 5px; +} diff --git a/chromium/chrome/browser/resources/options/chromeos/change_picture_options.html b/chromium/chrome/browser/resources/options/chromeos/change_picture_options.html new file mode 100644 index 00000000000..571555750df --- /dev/null +++ b/chromium/chrome/browser/resources/options/chromeos/change_picture_options.html @@ -0,0 +1,34 @@ +<div id="change-picture-page" class="page" hidden> + <div class="close-button"></div> + <h1 i18n-content="changePicturePage"></h1> + <div class="content-area"> + <div i18n-content="changePicturePageDescription"></div> + <div id="user-images-area"> + <grid id="user-image-grid" class="user-image-picker"></grid> + <div id="user-image-preview"> + <img id="user-image-preview-img" i18n-values="alt:previewAltText"> + <div class="user-image-stream-area"> + <div class="perspective-box"> + <div id="user-image-stream-crop"> + <video class="user-image-stream" autoplay></video> + </div> + </div> + <div class="spinner"></div> + </div> + <button id="flip-photo" class="custom-appearance"></button> + <button id="discard-photo"></button> + <button id="take-photo"></button> + </div> + </div> + <div id="user-image-attribution"> + <span i18n-content="authorCredit"></span> + <strong id="user-image-author-name"></strong> + <a id="user-image-author-website" target="_blank"></a> + </div> + </div> + <div class="action-area"> + <div class="button-strip"> + <button id="change-picture-overlay-confirm" i18n-content="done"></button> + </div> + </div> +</div> diff --git a/chromium/chrome/browser/resources/options/chromeos/change_picture_options.js b/chromium/chrome/browser/resources/options/chromeos/change_picture_options.js new file mode 100644 index 00000000000..51ed5364e26 --- /dev/null +++ b/chromium/chrome/browser/resources/options/chromeos/change_picture_options.js @@ -0,0 +1,320 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + + var OptionsPage = options.OptionsPage; + var UserImagesGrid = options.UserImagesGrid; + var ButtonImages = UserImagesGrid.ButtonImages; + + /** + * Array of button URLs used on this page. + * @type {Array.<string>} + * @const + */ + var ButtonImageUrls = [ + ButtonImages.TAKE_PHOTO, + ButtonImages.CHOOSE_FILE + ]; + + ///////////////////////////////////////////////////////////////////////////// + // ChangePictureOptions class: + + /** + * Encapsulated handling of ChromeOS change picture options page. + * @constructor + */ + function ChangePictureOptions() { + OptionsPage.call( + this, + 'changePicture', + loadTimeData.getString('changePicturePage'), + 'change-picture-page'); + } + + cr.addSingletonGetter(ChangePictureOptions); + + ChangePictureOptions.prototype = { + // Inherit ChangePictureOptions from OptionsPage. + __proto__: options.OptionsPage.prototype, + + /** + * Initializes ChangePictureOptions page. + */ + initializePage: function() { + // Call base class implementation to start preferences initialization. + OptionsPage.prototype.initializePage.call(this); + + var imageGrid = $('user-image-grid'); + UserImagesGrid.decorate(imageGrid); + + // Preview image will track the selected item's URL. + var previewElement = $('user-image-preview'); + previewElement.oncontextmenu = function(e) { e.preventDefault(); }; + + imageGrid.previewElement = previewElement; + imageGrid.selectionType = 'default'; + imageGrid.flipPhotoElement = $('flip-photo'); + + imageGrid.addEventListener('select', + this.handleImageSelected_.bind(this)); + imageGrid.addEventListener('activate', + this.handleImageActivated_.bind(this)); + imageGrid.addEventListener('phototaken', + this.handlePhotoTaken_.bind(this)); + imageGrid.addEventListener('photoupdated', + this.handlePhotoTaken_.bind(this)); + + // Set the title for "Take Photo" button. + imageGrid.cameraTitle = loadTimeData.getString('takePhoto'); + + // Add the "Choose file" button. + imageGrid.addItem(ButtonImages.CHOOSE_FILE, + loadTimeData.getString('chooseFile'), + this.handleChooseFile_.bind(this)).type = 'file'; + + // Profile image data. + this.profileImage_ = imageGrid.addItem( + ButtonImages.PROFILE_PICTURE, + loadTimeData.getString('profilePhotoLoading')); + this.profileImage_.type = 'profile'; + + $('take-photo').addEventListener( + 'click', this.handleTakePhoto_.bind(this)); + $('discard-photo').addEventListener( + 'click', imageGrid.discardPhoto.bind(imageGrid)); + + // Toggle 'animation' class for the duration of WebKit transition. + $('flip-photo').addEventListener( + 'click', function(e) { + previewElement.classList.add('animation'); + imageGrid.flipPhoto = !imageGrid.flipPhoto; + }); + $('user-image-stream-crop').addEventListener( + 'webkitTransitionEnd', function(e) { + previewElement.classList.remove('animation'); + }); + $('user-image-preview-img').addEventListener( + 'webkitTransitionEnd', function(e) { + previewElement.classList.remove('animation'); + }); + + // Old user image data (if present). + this.oldImage_ = null; + + $('change-picture-overlay-confirm').addEventListener( + 'click', this.closeOverlay_.bind(this)); + + chrome.send('onChangePicturePageInitialized'); + }, + + /** + * Called right after the page has been shown to user. + */ + didShowPage: function() { + var imageGrid = $('user-image-grid'); + // Reset camera element. + imageGrid.cameraImage = null; + imageGrid.updateAndFocus(); + chrome.send('onChangePicturePageShown'); + }, + + /** + * Called right before the page is hidden. + */ + willHidePage: function() { + var imageGrid = $('user-image-grid'); + imageGrid.blur(); // Make sure the image grid is not active. + imageGrid.stopCamera(); + if (this.oldImage_) { + imageGrid.removeItem(this.oldImage_); + this.oldImage_ = null; + } + }, + + /** + * Called right after the page has been hidden. + */ + // TODO(ivankr): both callbacks are required as only one of them is called + // depending on the way the page was closed, see http://crbug.com/118923. + didClosePage: function() { + this.willHidePage(); + }, + + /** + * Closes the overlay, returning to the main settings page. + * @private + */ + closeOverlay_: function() { + if (!$('change-picture-page').hidden) + OptionsPage.closeOverlay(); + }, + + /** + * Handles "Take photo" button click. + * @private + */ + handleTakePhoto_: function() { + $('user-image-grid').takePhoto(); + }, + + /** + * Handle photo captured event. + * @param {Event} e Event with 'dataURL' property containing a data URL. + */ + handlePhotoTaken_: function(e) { + chrome.send('photoTaken', [e.dataURL]); + }, + + /** + * Handles "Choose a file" button activation. + * @private + */ + handleChooseFile_: function() { + chrome.send('chooseFile'); + this.closeOverlay_(); + }, + + /** + * Handles image selection change. + * @param {Event} e Selection change Event. + * @private + */ + handleImageSelected_: function(e) { + var imageGrid = $('user-image-grid'); + var url = imageGrid.selectedItemUrl; + // Ignore selection change caused by program itself and selection of one + // of the action buttons. + if (!imageGrid.inProgramSelection && + url != ButtonImages.TAKE_PHOTO && url != ButtonImages.CHOOSE_FILE) { + chrome.send('selectImage', [url, imageGrid.selectionType]); + } + // Start/stop camera on (de)selection. + if (!imageGrid.inProgramSelection && + imageGrid.selectionType != e.oldSelectionType) { + if (imageGrid.selectionType == 'camera') { + imageGrid.startCamera( + function() { + // Start capture if camera is still the selected item. + return imageGrid.selectedItem == imageGrid.cameraImage; + }); + } else { + imageGrid.stopCamera(); + } + } + // Update image attribution text. + var image = imageGrid.selectedItem; + $('user-image-author-name').textContent = image.author; + $('user-image-author-website').textContent = image.website; + $('user-image-author-website').href = image.website; + $('user-image-attribution').style.visibility = + (image.author || image.website) ? 'visible' : 'hidden'; + }, + + /** + * Handles image activation (by pressing Enter). + * @private + */ + handleImageActivated_: function() { + switch ($('user-image-grid').selectedItemUrl) { + case ButtonImages.TAKE_PHOTO: + this.handleTakePhoto_(); + break; + case ButtonImages.CHOOSE_FILE: + this.handleChooseFile_(); + break; + default: + this.closeOverlay_(); + break; + } + }, + + /** + * Adds or updates old user image taken from file/camera (neither a profile + * image nor a default one). + * @param {string} imageUrl Old user image, as data or internal URL. + * @private + */ + setOldImage_: function(imageUrl) { + var imageGrid = $('user-image-grid'); + if (this.oldImage_) { + this.oldImage_ = imageGrid.updateItem(this.oldImage_, imageUrl); + } else { + // Insert next to the profile image. + var pos = imageGrid.indexOf(this.profileImage_) + 1; + this.oldImage_ = imageGrid.addItem(imageUrl, undefined, undefined, pos); + this.oldImage_.type = 'old'; + imageGrid.selectedItem = this.oldImage_; + } + }, + + /** + * Updates user's profile image. + * @param {string} imageUrl Profile image, encoded as data URL. + * @param {boolean} select If true, profile image should be selected. + * @private + */ + setProfileImage_: function(imageUrl, select) { + var imageGrid = $('user-image-grid'); + this.profileImage_ = imageGrid.updateItem( + this.profileImage_, imageUrl, loadTimeData.getString('profilePhoto')); + if (select) + imageGrid.selectedItem = this.profileImage_; + }, + + /** + * Selects user image with the given URL. + * @param {string} url URL of the image to select. + * @private + */ + setSelectedImage_: function(url) { + $('user-image-grid').selectedItemUrl = url; + }, + + /** + * @param {boolean} present Whether camera is detected. + */ + setCameraPresent_: function(present) { + $('user-image-grid').cameraPresent = present; + }, + + /** + * Appends default images to the image grid. Should only be called once. + * @param {Array.<{url: string, author: string, website: string}>} + * imagesData An array of default images data, including URL, author and + * website. + * @private + */ + setDefaultImages_: function(imagesData) { + var imageGrid = $('user-image-grid'); + for (var i = 0, data; data = imagesData[i]; i++) { + var item = imageGrid.addItem(data.url); + item.type = 'default'; + item.author = data.author || ''; + item.website = data.website || ''; + } + }, + }; + + // Forward public APIs to private implementations. + [ + 'closeOverlay', + 'setCameraPresent', + 'setDefaultImages', + 'setOldImage', + 'setProfileImage', + 'setSelectedImage', + ].forEach(function(name) { + ChangePictureOptions[name] = function() { + var instance = ChangePictureOptions.getInstance(); + return instance[name + '_'].apply(instance, arguments); + }; + }); + + // Export + return { + ChangePictureOptions: ChangePictureOptions + }; + +}); diff --git a/chromium/chrome/browser/resources/options/chromeos/display_options.css b/chromium/chrome/browser/resources/options/chromeos/display_options.css new file mode 100644 index 00000000000..3f738791e5c --- /dev/null +++ b/chromium/chrome/browser/resources/options/chromeos/display_options.css @@ -0,0 +1,96 @@ +/* Copyright (c) 2012 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ + +#display-options-page { + background-color: rgb(240, 240, 240); +} + +#display-options-content-area { + padding: 0; +} + +#display-options-displays-view-host { + padding: 20px 0 20px 0; +} + +#display-options-displays-view { + overflow: hidden; + position: relative; + width: 100%; +} + +#display-options-displays-view-mirroring { + margin: 20px 0 20px 0; +} + +#display-configurations { + -webkit-padding-end: 0; + -webkit-padding-start: 15px; + background-color: white; + border-top: 1px solid lightgrey; + padding-bottom: 15px; + padding-top: 15px; +} + +/* The arrow at the border #display-configurations to point the focused display. + * This is achieved by a square rotated by 45-deg, and it has border at the + * upper-half, which were left/top before the rotation. */ +#display-configuration-arrow { + -webkit-transform: rotate(45deg); + background-color: white; + border-left: 1px solid lightgrey; + border-top: 1px solid lightgrey; + height: 20px; + position: absolute; + width: 20px; + z-index: 1; +} + +#selected-display-data-container { + z-index: 2; +} + +#selected-display-name { + font-size: large; + font-weight: normal; + margin-top: 5px; + padding: 0; +} + +.selected-display-option-row { + margin-top: 10px; +} + +.selected-display-option-title { + display: inline-block; + margin-right: 10px; +} + +.displays-display { + -webkit-user-select: none; + background: rgb(240, 240, 240); + border: solid 1px; + box-sizing: border-box; + font-weight: bold; + position: absolute; + text-align: center; + z-index: 2; +} + +.display-mirrored { + border: solid 1px; +} + +.displays-focused { + border: solid 2px rgb(0, 138, 255); + color: rgb(0, 138, 255); +} + +#display-options-toggle-mirroring { + margin-right: 5px; +} + +.display-options-button { + width: 130px; +} diff --git a/chromium/chrome/browser/resources/options/chromeos/display_options.html b/chromium/chrome/browser/resources/options/chromeos/display_options.html new file mode 100644 index 00000000000..989e7ed3da0 --- /dev/null +++ b/chromium/chrome/browser/resources/options/chromeos/display_options.html @@ -0,0 +1,56 @@ +<div id="display-options-page" class="page" hidden> + <div class="close-button"></div> + <h1 i18n-content="displayOptionsPage"></h1> + <div class="content-area" id="display-options-content-area"> + <div id="display-options-displays-view-host"> + </div> + <div id="display-configurations"> + <div id="selected-display-data-container"> + <div id="selected-display-name"></div> + <div class="selected-display-option-row"> + <div class="selected-display-option-title" + i18n-content="selectedDisplayTitleOptions"> + </div> + <button id="display-options-toggle-mirroring" + class="display-options-button" i18n-content="startMirroring"> + </button> + <button id="display-options-set-primary" + class="display-options-button" i18n-content="setPrimary"> + </div> + <div class="selected-display-option-row"> + <div class="selected-display-option-title" + i18n-content="selectedDisplayTitleResolution"> + </div> + <select id="display-options-resolution-selection" + class="display-options-button"> + </select> + </div> + <div class="selected-display-option-row"> + <div class="selected-display-option-title" + i18n-content="selectedDisplayTitleOrientation"> + </div> + <select id="display-options-orientation-selection" + class="display-options-button"> + <option value="0" i18n-content="orientation0"></option> + <option value="90" i18n-content="orientation90"></option> + <option value="180" i18n-content="orientation180"></option> + <option value="270" i18n-content="orientation270"></option> + </select> + </div> + <div class="selected-display-option-row"> + <div class="selected-display-option-title" + i18n-content="selectedDisplayTitleOverscan"> + </div> + <button id="selected-display-start-calibrating-overscan" + class="display-options-button" + i18n-content="startCalibratingOverscan"> + </button> + </div> + </div> + </div> + <!-- The arrow of display-configuration is achieved by a div + rotated by 45deg. --> + <div id="display-configuration-arrow"> + </div> + </div> +</div> diff --git a/chromium/chrome/browser/resources/options/chromeos/display_options.js b/chromium/chrome/browser/resources/options/chromeos/display_options.js new file mode 100644 index 00000000000..3b000ca283e --- /dev/null +++ b/chromium/chrome/browser/resources/options/chromeos/display_options.js @@ -0,0 +1,863 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + var OptionsPage = options.OptionsPage; + + // The scale ratio of the display rectangle to its original size. + /** @const */ var VISUAL_SCALE = 1 / 10; + + // The number of pixels to share the edges between displays. + /** @const */ var MIN_OFFSET_OVERLAP = 5; + + /** + * Enumeration of secondary display layout. The value has to be same as the + * values in ash/display/display_controller.cc. + * @enum {number} + */ + var SecondaryDisplayLayout = { + TOP: 0, + RIGHT: 1, + BOTTOM: 2, + LEFT: 3 + }; + + /** + * Calculates the bounds of |element| relative to the page. + * @param {HTMLElement} element The element to be known. + * @return {Object} The object for the bounds, with x, y, width, and height. + */ + function getBoundsInPage(element) { + var bounds = { + x: element.offsetLeft, + y: element.offsetTop, + width: element.offsetWidth, + height: element.offsetHeight + }; + var parent = element.offsetParent; + while (parent && parent != document.body) { + bounds.x += parent.offsetLeft; + bounds.y += parent.offsetTop; + parent = parent.offsetParent; + } + return bounds; + } + + /** + * Gets the position of |point| to |rect|, left, right, top, or bottom. + * @param {Object} rect The base rectangle with x, y, width, and height. + * @param {Object} point The point to check the position. + * @return {SecondaryDisplayLayout} The position of the calculated point. + */ + function getPositionToRectangle(rect, point) { + // Separates the area into four (LEFT/RIGHT/TOP/BOTTOM) by the diagonals of + // the rect, and decides which area the display should reside. + var diagonalSlope = rect.height / rect.width; + var topDownIntercept = rect.y - rect.x * diagonalSlope; + var bottomUpIntercept = rect.y + rect.height + rect.x * diagonalSlope; + + if (point.y > topDownIntercept + point.x * diagonalSlope) { + if (point.y > bottomUpIntercept - point.x * diagonalSlope) + return SecondaryDisplayLayout.BOTTOM; + else + return SecondaryDisplayLayout.LEFT; + } else { + if (point.y > bottomUpIntercept - point.x * diagonalSlope) + return SecondaryDisplayLayout.RIGHT; + else + return SecondaryDisplayLayout.TOP; + } + } + + /** + * Encapsulated handling of the 'Display' page. + * @constructor + */ + function DisplayOptions() { + OptionsPage.call(this, 'display', + loadTimeData.getString('displayOptionsPageTabTitle'), + 'display-options-page'); + } + + cr.addSingletonGetter(DisplayOptions); + + DisplayOptions.prototype = { + __proto__: OptionsPage.prototype, + + /** + * Whether the current output status is mirroring displays or not. + * @private + */ + mirroring_: false, + + /** + * The current secondary display layout. + * @private + */ + layout_: SecondaryDisplayLayout.RIGHT, + + /** + * The array of current output displays. It also contains the display + * rectangles currently rendered on screen. + * @private + */ + displays_: [], + + /** + * The index for the currently focused display in the options UI. null if + * no one has focus. + * @private + */ + focusedIndex_: null, + + /** + * The primary display. + * @private + */ + primaryDisplay_: null, + + /** + * The secondary display. + * @private + */ + secondaryDisplay_: null, + + /** + * The container div element which contains all of the display rectangles. + * @private + */ + displaysView_: null, + + /** + * The scale factor of the actual display size to the drawn display + * rectangle size. + * @private + */ + visualScale_: VISUAL_SCALE, + + /** + * The location where the last touch event happened. This is used to + * prevent unnecessary dragging events happen. Set to null unless it's + * during touch events. + * @private + */ + lastTouchLocation_: null, + + /** + * Initialize the page. + */ + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + + $('display-options-toggle-mirroring').onclick = function() { + this.mirroring_ = !this.mirroring_; + chrome.send('setMirroring', [this.mirroring_]); + }.bind(this); + + var container = $('display-options-displays-view-host'); + container.onmousemove = this.onMouseMove_.bind(this); + window.addEventListener('mouseup', this.endDragging_.bind(this), true); + container.ontouchmove = this.onTouchMove_.bind(this); + container.ontouchend = this.endDragging_.bind(this); + + $('display-options-set-primary').onclick = function() { + chrome.send('setPrimary', [this.displays_[this.focusedIndex_].id]); + }.bind(this); + $('display-options-resolution-selection').onchange = function(ev) { + var display = this.displays_[this.focusedIndex_]; + var resolution = display.resolutions[ev.target.value]; + if (resolution.scale) { + chrome.send('setUIScale', [display.id, resolution.scale]); + } else { + chrome.send('setResolution', + [display.id, resolution.width, resolution.height]); + } + }.bind(this); + $('display-options-orientation-selection').onchange = function(ev) { + chrome.send('setOrientation', [this.displays_[this.focusedIndex_].id, + ev.target.value]); + }.bind(this); + $('selected-display-start-calibrating-overscan').onclick = function() { + // Passes the target display ID. Do not specify it through URL hash, + // we do not care back/forward. + var displayOverscan = options.DisplayOverscan.getInstance(); + displayOverscan.setDisplayId(this.displays_[this.focusedIndex_].id); + OptionsPage.navigateToPage('displayOverscan'); + }.bind(this); + + chrome.send('getDisplayInfo'); + }, + + /** @override */ + didShowPage: function() { + var optionTitles = document.getElementsByClassName( + 'selected-display-option-title'); + var maxSize = 0; + for (var i = 0; i < optionTitles.length; i++) + maxSize = Math.max(maxSize, optionTitles[i].clientWidth); + for (var i = 0; i < optionTitles.length; i++) + optionTitles[i].style.width = maxSize + 'px'; + }, + + /** @override */ + onVisibilityChanged_: function() { + OptionsPage.prototype.onVisibilityChanged_(this); + if (this.visible) + chrome.send('getDisplayInfo'); + }, + + /** + * Mouse move handler for dragging display rectangle. + * @param {Event} e The mouse move event. + * @private + */ + onMouseMove_: function(e) { + return this.processDragging_(e, {x: e.pageX, y: e.pageY}); + }, + + /** + * Touch move handler for dragging display rectangle. + * @param {Event} e The touch move event. + * @private + */ + onTouchMove_: function(e) { + if (e.touches.length != 1) + return true; + + var touchLocation = {x: e.touches[0].pageX, y: e.touches[0].pageY}; + // Touch move events happen even if the touch location doesn't change, but + // it doesn't need to process the dragging. Since sometimes the touch + // position changes slightly even though the user doesn't think to move + // the finger, very small move is just ignored. + /** @const */ var IGNORABLE_TOUCH_MOVE_PX = 1; + var xDiff = Math.abs(touchLocation.x - this.lastTouchLocation_.x); + var yDiff = Math.abs(touchLocation.y - this.lastTouchLocation_.y); + if (xDiff <= IGNORABLE_TOUCH_MOVE_PX && + yDiff <= IGNORABLE_TOUCH_MOVE_PX) { + return true; + } + + this.lastTouchLocation_ = touchLocation; + return this.processDragging_(e, touchLocation); + }, + + /** + * Mouse down handler for dragging display rectangle. + * @param {Event} e The mouse down event. + * @private + */ + onMouseDown_: function(e) { + if (this.mirroring_) + return true; + + if (e.button != 0) + return true; + + e.preventDefault(); + return this.startDragging_(e.target, {x: e.pageX, y: e.pageY}); + }, + + /** + * Touch start handler for dragging display rectangle. + * @param {Event} e The touch start event. + * @private + */ + onTouchStart_: function(e) { + if (this.mirroring_) + return true; + + if (e.touches.length != 1) + return false; + + e.preventDefault(); + var touch = e.touches[0]; + this.lastTouchLocation_ = {x: touch.pageX, y: touch.pageY}; + return this.startDragging_(e.target, this.lastTouchLocation_); + }, + + /** + * Collects the current data and sends it to Chrome. + * @private + */ + applyResult_: function() { + // Offset is calculated from top or left edge. + var primary = this.primaryDisplay_; + var secondary = this.secondaryDisplay_; + var offset; + if (this.layout_ == SecondaryDisplayLayout.LEFT || + this.layout_ == SecondaryDisplayLayout.RIGHT) { + offset = secondary.div.offsetTop - primary.div.offsetTop; + } else { + offset = secondary.div.offsetLeft - primary.div.offsetLeft; + } + chrome.send('setDisplayLayout', + [this.layout_, offset / this.visualScale_]); + }, + + /** + * Snaps the region [point, width] to [basePoint, baseWidth] if + * the [point, width] is close enough to the base's edge. + * @param {number} point The starting point of the region. + * @param {number} width The width of the region. + * @param {number} basePoint The starting point of the base region. + * @param {number} baseWidth The width of the base region. + * @return {number} The moved point. Returns point itself if it doesn't + * need to snap to the edge. + * @private + */ + snapToEdge_: function(point, width, basePoint, baseWidth) { + // If the edge of the regions is smaller than this, it will snap to the + // base's edge. + /** @const */ var SNAP_DISTANCE_PX = 16; + + var startDiff = Math.abs(point - basePoint); + var endDiff = Math.abs(point + width - (basePoint + baseWidth)); + // Prefer the closer one if both edges are close enough. + if (startDiff < SNAP_DISTANCE_PX && startDiff < endDiff) + return basePoint; + else if (endDiff < SNAP_DISTANCE_PX) + return basePoint + baseWidth - width; + + return point; + }, + + /** + * Processes the actual dragging of display rectangle. + * @param {Event} e The event which triggers this drag. + * @param {Object} eventLocation The location where the event happens. + * @private + */ + processDragging_: function(e, eventLocation) { + if (!this.dragging_) + return true; + + var index = -1; + for (var i = 0; i < this.displays_.length; i++) { + if (this.displays_[i] == this.dragging_.display) { + index = i; + break; + } + } + if (index < 0) + return true; + + e.preventDefault(); + + // Note that current code of moving display-rectangles doesn't work + // if there are >=3 displays. This is our assumption for M21. + // TODO(mukai): Fix the code to allow >=3 displays. + var newPosition = { + x: this.dragging_.originalLocation.x + + (eventLocation.x - this.dragging_.eventLocation.x), + y: this.dragging_.originalLocation.y + + (eventLocation.y - this.dragging_.eventLocation.y) + }; + + var baseDiv = this.dragging_.display.isPrimary ? + this.secondaryDisplay_.div : this.primaryDisplay_.div; + var draggingDiv = this.dragging_.display.div; + + newPosition.x = this.snapToEdge_(newPosition.x, draggingDiv.offsetWidth, + baseDiv.offsetLeft, baseDiv.offsetWidth); + newPosition.y = this.snapToEdge_(newPosition.y, draggingDiv.offsetHeight, + baseDiv.offsetTop, baseDiv.offsetHeight); + + var newCenter = { + x: newPosition.x + draggingDiv.offsetWidth / 2, + y: newPosition.y + draggingDiv.offsetHeight / 2 + }; + + var baseBounds = { + x: baseDiv.offsetLeft, + y: baseDiv.offsetTop, + width: baseDiv.offsetWidth, + height: baseDiv.offsetHeight + }; + switch (getPositionToRectangle(baseBounds, newCenter)) { + case SecondaryDisplayLayout.RIGHT: + this.layout_ = this.dragging_.display.isPrimary ? + SecondaryDisplayLayout.LEFT : SecondaryDisplayLayout.RIGHT; + break; + case SecondaryDisplayLayout.LEFT: + this.layout_ = this.dragging_.display.isPrimary ? + SecondaryDisplayLayout.RIGHT : SecondaryDisplayLayout.LEFT; + break; + case SecondaryDisplayLayout.TOP: + this.layout_ = this.dragging_.display.isPrimary ? + SecondaryDisplayLayout.BOTTOM : SecondaryDisplayLayout.TOP; + break; + case SecondaryDisplayLayout.BOTTOM: + this.layout_ = this.dragging_.display.isPrimary ? + SecondaryDisplayLayout.TOP : SecondaryDisplayLayout.BOTTOM; + break; + } + + if (this.layout_ == SecondaryDisplayLayout.LEFT || + this.layout_ == SecondaryDisplayLayout.RIGHT) { + if (newPosition.y > baseDiv.offsetTop + baseDiv.offsetHeight) + this.layout_ = this.dragging_.display.isPrimary ? + SecondaryDisplayLayout.TOP : SecondaryDisplayLayout.BOTTOM; + else if (newPosition.y + draggingDiv.offsetHeight < + baseDiv.offsetTop) + this.layout_ = this.dragging_.display.isPrimary ? + SecondaryDisplayLayout.BOTTOM : SecondaryDisplayLayout.TOP; + } else { + if (newPosition.x > baseDiv.offsetLeft + baseDiv.offsetWidth) + this.layout_ = this.dragging_.display.isPrimary ? + SecondaryDisplayLayout.LEFT : SecondaryDisplayLayout.RIGHT; + else if (newPosition.x + draggingDiv.offsetWidth < + baseDiv.offstLeft) + this.layout_ = this.dragging_.display.isPrimary ? + SecondaryDisplayLayout.RIGHT : SecondaryDisplayLayout.LEFT; + } + + var layoutToBase; + if (!this.dragging_.display.isPrimary) { + layoutToBase = this.layout_; + } else { + switch (this.layout_) { + case SecondaryDisplayLayout.RIGHT: + layoutToBase = SecondaryDisplayLayout.LEFT; + break; + case SecondaryDisplayLayout.LEFT: + layoutToBase = SecondaryDisplayLayout.RIGHT; + break; + case SecondaryDisplayLayout.TOP: + layoutToBase = SecondaryDisplayLayout.BOTTOM; + break; + case SecondaryDisplayLayout.BOTTOM: + layoutToBase = SecondaryDisplayLayout.TOP; + break; + } + } + + switch (layoutToBase) { + case SecondaryDisplayLayout.RIGHT: + draggingDiv.style.left = + baseDiv.offsetLeft + baseDiv.offsetWidth + 'px'; + draggingDiv.style.top = newPosition.y + 'px'; + break; + case SecondaryDisplayLayout.LEFT: + draggingDiv.style.left = + baseDiv.offsetLeft - draggingDiv.offsetWidth + 'px'; + draggingDiv.style.top = newPosition.y + 'px'; + break; + case SecondaryDisplayLayout.TOP: + draggingDiv.style.top = + baseDiv.offsetTop - draggingDiv.offsetHeight + 'px'; + draggingDiv.style.left = newPosition.x + 'px'; + break; + case SecondaryDisplayLayout.BOTTOM: + draggingDiv.style.top = + baseDiv.offsetTop + baseDiv.offsetHeight + 'px'; + draggingDiv.style.left = newPosition.x + 'px'; + break; + } + + return false; + }, + + /** + * start dragging of a display rectangle. + * @param {HTMLElement} target The event target. + * @param {Object} eventLocation The object to hold the location where + * this event happens. + * @private + */ + startDragging_: function(target, eventLocation) { + this.focusedIndex_ = null; + for (var i = 0; i < this.displays_.length; i++) { + var display = this.displays_[i]; + if (display.div == target || + (target.offsetParent && target.offsetParent == display.div)) { + this.focusedIndex_ = i; + break; + } + } + + for (var i = 0; i < this.displays_.length; i++) { + var display = this.displays_[i]; + display.div.className = 'displays-display'; + if (i != this.focusedIndex_) + continue; + + display.div.classList.add('displays-focused'); + if (this.displays_.length > 1) { + this.dragging_ = { + display: display, + originalLocation: { + x: display.div.offsetLeft, y: display.div.offsetTop + }, + eventLocation: eventLocation + }; + } + } + + this.updateSelectedDisplayDescription_(); + return false; + }, + + /** + * finish the current dragging of displays. + * @param {Event} e The event which triggers this. + * @private + */ + endDragging_: function(e) { + this.lastTouchLocation_ = null; + if (this.dragging_) { + // Make sure the dragging location is connected. + var baseDiv = this.dragging_.display.isPrimary ? + this.secondaryDisplay_.div : this.primaryDisplay_.div; + var draggingDiv = this.dragging_.display.div; + if (this.layout_ == SecondaryDisplayLayout.LEFT || + this.layout_ == SecondaryDisplayLayout.RIGHT) { + var top = Math.max(draggingDiv.offsetTop, + baseDiv.offsetTop - draggingDiv.offsetHeight + + MIN_OFFSET_OVERLAP); + top = Math.min(top, + baseDiv.offsetTop + baseDiv.offsetHeight - + MIN_OFFSET_OVERLAP); + draggingDiv.style.top = top + 'px'; + } else { + var left = Math.max(draggingDiv.offsetLeft, + baseDiv.offsetLeft - draggingDiv.offsetWidth + + MIN_OFFSET_OVERLAP); + left = Math.min(left, + baseDiv.offsetLeft + baseDiv.offsetWidth - + MIN_OFFSET_OVERLAP); + draggingDiv.style.left = left + 'px'; + } + var originalPosition = this.dragging_.display.originalPosition; + if (originalPosition.x != draggingDiv.offsetLeft || + originalPosition.y != draggingDiv.offsetTop) + this.applyResult_(); + this.dragging_ = null; + } + this.updateSelectedDisplayDescription_(); + return false; + }, + + /** + * Updates the description of selected display section for mirroring mode. + * @private + */ + updateSelectedDisplaySectionMirroring_: function() { + $('display-configuration-arrow').hidden = true; + $('display-options-set-primary').disabled = true; + $('display-options-toggle-mirroring').disabled = false; + $('selected-display-start-calibrating-overscan').disabled = true; + $('display-options-orientation-selection').disabled = true; + var display = this.displays_[0]; + $('selected-display-name').textContent = + loadTimeData.getString('mirroringDisplay'); + var resolution = $('display-options-resolution-selection'); + var option = document.createElement('option'); + option.value = 'default'; + option.textContent = display.width + 'x' + display.height; + resolution.appendChild(option); + resolution.disabled = true; + }, + + /** + * Updates the description of selected display section when no display is + * selected. + * @private + */ + updateSelectedDisplaySectionNoSelected_: function() { + $('display-configuration-arrow').hidden = true; + $('display-options-set-primary').disabled = true; + $('display-options-toggle-mirroring').disabled = true; + $('selected-display-start-calibrating-overscan').disabled = true; + $('display-options-orientation-selection').disabled = true; + $('selected-display-name').textContent = ''; + var resolution = $('display-options-resolution-selection'); + resolution.appendChild(document.createElement('option')); + resolution.disabled = true; + }, + + /** + * Updates the description of selected display section for the selected + * display. + * @param {Object} display The selected display object. + * @private + */ + updateSelectedDisplaySectionForDisplay_: function(display) { + var arrow = $('display-configuration-arrow'); + arrow.hidden = false; + // Adding 1 px to the position to fit the border line and the border in + // arrow precisely. + arrow.style.top = $('display-configurations').offsetTop - + arrow.offsetHeight / 2 + 'px'; + arrow.style.left = display.div.offsetLeft + + display.div.offsetWidth / 2 - arrow.offsetWidth / 2 + 'px'; + + $('display-options-set-primary').disabled = display.isPrimary; + $('display-options-toggle-mirroring').disabled = + (this.displays_.length <= 1); + $('selected-display-start-calibrating-overscan').disabled = + display.isInternal; + + var orientation = $('display-options-orientation-selection'); + orientation.disabled = false; + var orientationOptions = orientation.getElementsByTagName('option'); + orientationOptions[display.orientation].selected = true; + + $('selected-display-name').textContent = display.name; + + var resolution = $('display-options-resolution-selection'); + if (display.resolutions.length <= 1) { + var option = document.createElement('option'); + option.value = 'default'; + option.textContent = display.width + 'x' + display.height; + option.selected = true; + resolution.appendChild(option); + resolution.disabled = true; + } else { + for (var i = 0; i < display.resolutions.length; i++) { + var option = document.createElement('option'); + option.value = i; + option.textContent = display.resolutions[i].width + 'x' + + display.resolutions[i].height; + if (display.resolutions[i].isBest) { + option.textContent += ' ' + + loadTimeData.getString('annotateBest'); + } + option.selected = display.resolutions[i].selected; + resolution.appendChild(option); + } + resolution.disabled = (display.resolutions.length <= 1); + } + }, + + /** + * Updates the description of the selected display section. + * @private + */ + updateSelectedDisplayDescription_: function() { + var resolution = $('display-options-resolution-selection'); + resolution.textContent = ''; + var orientation = $('display-options-orientation-selection'); + var orientationOptions = orientation.getElementsByTagName('option'); + for (var i = 0; i < orientationOptions.length; i++) + orientationOptions.selected = false; + + if (this.mirroring_) { + this.updateSelectedDisplaySectionMirroring_(); + } else if (this.focusedIndex_ == null || + this.displays_[this.focusedIndex_] == null) { + this.updateSelectedDisplaySectionNoSelected_(); + } else { + this.updateSelectedDisplaySectionForDisplay_( + this.displays_[this.focusedIndex_]); + } + }, + + /** + * Clears the drawing area for display rectangles. + * @private + */ + resetDisplaysView_: function() { + var displaysViewHost = $('display-options-displays-view-host'); + displaysViewHost.removeChild(displaysViewHost.firstChild); + this.displaysView_ = document.createElement('div'); + this.displaysView_.id = 'display-options-displays-view'; + displaysViewHost.appendChild(this.displaysView_); + }, + + /** + * Lays out the display rectangles for mirroring. + * @private + */ + layoutMirroringDisplays_: function() { + // Offset pixels for secondary display rectangles. The offset includes the + // border width. + /** @const */ var MIRRORING_OFFSET_PIXELS = 3; + // Always show two displays because there must be two displays when + // the display_options is enabled. Don't rely on displays_.length because + // there is only one display from chrome's perspective in mirror mode. + /** @const */ var MIN_NUM_DISPLAYS = 2; + /** @const */ var MIRRORING_VERTICAL_MARGIN = 20; + + // The width/height should be same as the first display: + var width = Math.ceil(this.displays_[0].width * this.visualScale_); + var height = Math.ceil(this.displays_[0].height * this.visualScale_); + + var numDisplays = Math.max(MIN_NUM_DISPLAYS, this.displays_.length); + + var totalWidth = width + numDisplays * MIRRORING_OFFSET_PIXELS; + var totalHeight = height + numDisplays * MIRRORING_OFFSET_PIXELS; + + this.displaysView_.style.height = totalHeight + 'px'; + this.displaysView_.classList.add( + 'display-options-displays-view-mirroring'); + + // The displays should be centered. + var offsetX = + $('display-options-displays-view').offsetWidth / 2 - totalWidth / 2; + + for (var i = 0; i < numDisplays; i++) { + var div = document.createElement('div'); + div.className = 'displays-display'; + div.style.top = i * MIRRORING_OFFSET_PIXELS + 'px'; + div.style.left = i * MIRRORING_OFFSET_PIXELS + offsetX + 'px'; + div.style.width = width + 'px'; + div.style.height = height + 'px'; + div.style.zIndex = i; + // set 'display-mirrored' class for the background display rectangles. + if (i != numDisplays - 1) + div.classList.add('display-mirrored'); + this.displaysView_.appendChild(div); + } + }, + + /** + * Layouts the display rectangles according to the current layout_. + * @private + */ + layoutDisplays_: function() { + var maxWidth = 0; + var maxHeight = 0; + var boundingBox = {left: 0, right: 0, top: 0, bottom: 0}; + for (var i = 0; i < this.displays_.length; i++) { + var display = this.displays_[i]; + boundingBox.left = Math.min(boundingBox.left, display.x); + boundingBox.right = Math.max( + boundingBox.right, display.x + display.width); + boundingBox.top = Math.min(boundingBox.top, display.y); + boundingBox.bottom = Math.max( + boundingBox.bottom, display.y + display.height); + maxWidth = Math.max(maxWidth, display.width); + maxHeight = Math.max(maxHeight, display.height); + } + + // Make the margin around the bounding box. + var areaWidth = boundingBox.right - boundingBox.left + maxWidth; + var areaHeight = boundingBox.bottom - boundingBox.top + maxHeight; + + // Calculates the scale by the width since horizontal size is more strict. + // TODO(mukai): Adds the check of vertical size in case. + this.visualScale_ = Math.min( + VISUAL_SCALE, this.displaysView_.offsetWidth / areaWidth); + + // Prepare enough area for thisplays_view by adding the maximum height. + this.displaysView_.style.height = + Math.ceil(areaHeight * this.visualScale_) + 'px'; + + var boundingCenter = { + x: Math.floor((boundingBox.right + boundingBox.left) * + this.visualScale_ / 2), + y: Math.floor((boundingBox.bottom + boundingBox.top) * + this.visualScale_ / 2) + }; + + // Centering the bounding box of the display rectangles. + var offset = { + x: Math.floor(this.displaysView_.offsetWidth / 2 - + (boundingBox.right + boundingBox.left) * this.visualScale_ / 2), + y: Math.floor(this.displaysView_.offsetHeight / 2 - + (boundingBox.bottom + boundingBox.top) * this.visualScale_ / 2) + }; + + for (var i = 0; i < this.displays_.length; i++) { + var display = this.displays_[i]; + var div = document.createElement('div'); + display.div = div; + + div.className = 'displays-display'; + if (i == this.focusedIndex_) + div.classList.add('displays-focused'); + + if (display.isPrimary) { + this.primaryDisplay_ = display; + } else { + this.secondaryDisplay_ = display; + } + var displayNameContainer = document.createElement('div'); + displayNameContainer.textContent = display.name; + div.appendChild(displayNameContainer); + display.nameContainer = displayNameContainer; + display.div.style.width = + Math.floor(display.width * this.visualScale_) + 'px'; + var newHeight = Math.floor(display.height * this.visualScale_); + display.div.style.height = newHeight + 'px'; + div.style.left = + Math.floor(display.x * this.visualScale_) + offset.x + 'px'; + div.style.top = + Math.floor(display.y * this.visualScale_) + offset.y + 'px'; + display.nameContainer.style.marginTop = + (newHeight - display.nameContainer.offsetHeight) / 2 + 'px'; + + div.onmousedown = this.onMouseDown_.bind(this); + div.ontouchstart = this.onTouchStart_.bind(this); + + this.displaysView_.appendChild(div); + + // Set the margin top to place the display name at the middle of the + // rectangle. Note that this has to be done after it's added into the + // |displaysView_|. Otherwise its offsetHeight is yet 0. + displayNameContainer.style.marginTop = + (div.offsetHeight - displayNameContainer.offsetHeight) / 2 + 'px'; + display.originalPosition = {x: div.offsetLeft, y: div.offsetTop}; + } + }, + + /** + * Called when the display arrangement has changed. + * @param {boolean} mirroring Whether current mode is mirroring or not. + * @param {Array} displays The list of the display information. + * @param {SecondaryDisplayLayout} layout The layout strategy. + * @param {number} offset The offset of the secondary display. + * @private + */ + onDisplayChanged_: function(mirroring, displays, layout, offset) { + if (!this.visible) + return; + + var hasExternal = false; + for (var i = 0; i < displays.length; i++) { + if (!displays[i].isInternal) { + hasExternal = true; + break; + } + } + + this.layout_ = layout; + + $('display-options-toggle-mirroring').textContent = + loadTimeData.getString( + mirroring ? 'stopMirroring' : 'startMirroring'); + + // Focus to the first display next to the primary one when |displays| list + // is updated. + if (mirroring) { + this.focusedIndex_ = null; + } else if (this.mirroring_ != mirroring || + this.displays_.length != displays.length) { + this.focusedIndex_ = 0; + } + + this.mirroring_ = mirroring; + this.displays_ = displays; + + this.resetDisplaysView_(); + if (this.mirroring_) + this.layoutMirroringDisplays_(); + else + this.layoutDisplays_(); + + this.updateSelectedDisplayDescription_(); + } + }; + + DisplayOptions.setDisplayInfo = function( + mirroring, displays, layout, offset) { + DisplayOptions.getInstance().onDisplayChanged_( + mirroring, displays, layout, offset); + }; + + // Export + return { + DisplayOptions: DisplayOptions + }; +}); diff --git a/chromium/chrome/browser/resources/options/chromeos/display_overscan.css b/chromium/chrome/browser/resources/options/chromeos/display_overscan.css new file mode 100644 index 00000000000..3a09e408e12 --- /dev/null +++ b/chromium/chrome/browser/resources/options/chromeos/display_overscan.css @@ -0,0 +1,63 @@ +/* Copyright (c) 2013 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ + +#display-overscan-content-area { + margin: 20px; + padding: 0; +} + +#display-overscan-operations-table { + position: absolute; + width: 100%; +} + +#display-overscan-operations-table td { + font-size: 12px; + text-align: center; + width: 50%; +} + +.display-overscan-operation-image { + padding-bottom: 30px; +} + +#display-overscan-operation-images-row { + vertical-align: middle; +} + +#display-overscan-operation-arrows { + background-image: -webkit-image-set( + url('overscan_arrows.png') 1x, + url('overscan_arrows_2x.png') 2x); + background-position: center; + background-repeat: no-repeat; + height: 51px; + width: 100%; +} + +#display-overscan-operation-shift { + background-image: -webkit-image-set( + url('overscan_shift.png') 1x, + url('overscan_shift_2x.png') 2x); + background-position: center; + background-repeat: no-repeat; + height: 23px; + width: 100%; +} + +html[dir=rtl] #display-overscan-operation-shift { + background-image: -webkit-image-set( + url('overscan_shift_rtl.png') 1x, + url('overscan_shift_rtl_2x.png') 2x); +} + +#display-overscan-button-strip { + bottom: 0; + position: absolute; + width: 100%; +} + +#display-overscan-buttons-spacer { + -webkit-box-flex: 1; +} diff --git a/chromium/chrome/browser/resources/options/chromeos/display_overscan.html b/chromium/chrome/browser/resources/options/chromeos/display_overscan.html new file mode 100644 index 00000000000..53c280affad --- /dev/null +++ b/chromium/chrome/browser/resources/options/chromeos/display_overscan.html @@ -0,0 +1,31 @@ +<div id="display-overscan-page" class="page" hidden> + <div class="close-button"></div> + <div class="content-area" id="display-overscan-content-area"> + <table id="display-overscan-operations-table"> + <tr id="display-overscan-operation-images-row"> + <td class="display-overscan-operation-image"> + <div id="display-overscan-operation-arrows"></div></td> + <td class="display-overscan-operation-image"> + <div id="display-overscan-operation-shift"></div></td> + </tr> + <tr> + <td><span i18n-content="shrinkAndExpand"></span></td> + <td><span i18n-content="move"></span></td> + </tr> + </table> + <!-- Specify 'reversed' to prevernt re-reversing the button order by + options_page. --> + <div class="button-strip" id="display-overscan-button-strip" reversed> + <button id="display-overscan-operation-reset" + i18n-content="overscanReset"> + </button> + <div id="display-overscan-buttons-spacer"></div> + <button id="display-overscan-operation-ok" + i18n-content="overscanOK"> + </button> + <button id="display-overscan-operation-cancel" + i18n-content="overscanCancel"> + </button> + </div> + </div> +</div> diff --git a/chromium/chrome/browser/resources/options/chromeos/display_overscan.js b/chromium/chrome/browser/resources/options/chromeos/display_overscan.js new file mode 100644 index 00000000000..0060d6cd6ff --- /dev/null +++ b/chromium/chrome/browser/resources/options/chromeos/display_overscan.js @@ -0,0 +1,160 @@ +// Copyright (c) 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + var OptionsPage = options.OptionsPage; + + /** + * Encapsulated handling of the 'DisplayOverscan' page. + * @constructor + */ + function DisplayOverscan() { + OptionsPage.call(this, 'displayOverscan', + loadTimeData.getString('displayOverscanPageTabTitle'), + 'display-overscan-page'); + } + + cr.addSingletonGetter(DisplayOverscan); + + DisplayOverscan.prototype = { + __proto__: OptionsPage.prototype, + + /** + * The ID of the target display. + * @private + */ + id_: null, + + /** + * The keyboard event handler function. + * @private + */ + keyHandler_: null, + + /** + * Initialize the page. + */ + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + + this.keyHandler_ = this.handleKeyevent_.bind(this); + $('display-overscan-operation-reset').onclick = function() { + chrome.send('reset'); + }; + $('display-overscan-operation-ok').onclick = function() { + chrome.send('commit'); + OptionsPage.closeOverlay(); + }; + $('display-overscan-operation-cancel').onclick = function() { + OptionsPage.cancelOverlay(); + }; + }, + + /** @override */ + handleCancel: function() { + // signals the cancel event. + chrome.send('cancel'); + OptionsPage.closeOverlay(); + }, + + /** @override */ + didShowPage: function() { + if (this.id_ == null) { + OptionsPage.cancelOverlay(); + return; + } + + window.addEventListener('keydown', this.keyHandler_); + // Sets up the size of the overscan dialog based on DisplayOptions dialog. + var displayOptionsPage = $('display-options-page'); + var displayOverscanPage = $('display-overscan-page'); + displayOverscanPage.style.width = + displayOptionsPage.offsetWidth - 20 + 'px'; + displayOverscanPage.style.minWidth = displayOverscanPage.style.width; + displayOverscanPage.style.height = + displayOptionsPage.offsetHeight - 50 + 'px'; + + // Moves the table to describe operation at the middle of the contents + // vertically. + var operationsTable = $('display-overscan-operations-table'); + var buttonsContainer = $('display-overscan-button-strip'); + operationsTable.style.top = buttonsContainer.offsetTop / 2 - + operationsTable.offsetHeight / 2 + 'px'; + + $('display-overscan-operation-cancel').focus(); + chrome.send('start', [this.id_]); + }, + + /** @override */ + didClosePage: function() { + window.removeEventListener('keydown', this.keyHandler_); + }, + + /** + * Called when the overscan calibration is canceled at the system level, + * such like the display is disconnected. + * @private + */ + onOverscanCanceled_: function() { + if (OptionsPage.getTopmostVisiblePage() == this) + OptionsPage.cancelOverlay(); + }, + + /** + * Sets the target display id. This method has to be called before + * navigating to this page. + * @param {string} id The target display id. + */ + setDisplayId: function(id) { + this.id_ = id; + }, + + /** + * Key event handler to make the effect of display rectangle. + * @param {Event} event The keyboard event. + * @private + */ + handleKeyevent_: function(event) { + switch (event.keyCode) { + case 37: // left arrow + if (event.shiftKey) + chrome.send('move', ['horizontal', -1]); + else + chrome.send('resize', ['horizontal', -1]); + event.preventDefault(); + break; + case 38: // up arrow + if (event.shiftKey) + chrome.send('move', ['vertical', -1]); + else + chrome.send('resize', ['vertical', -1]); + event.preventDefault(); + break; + case 39: // right arrow + if (event.shiftKey) + chrome.send('move', ['horizontal', 1]); + else + chrome.send('resize', ['horizontal', 1]); + event.preventDefault(); + break; + case 40: // bottom arrow + if (event.shiftKey) + chrome.send('move', ['vertical', 1]); + else + chrome.send('resize', ['vertical', 1]); + event.preventDefault(); + break; + } + } + }; + + DisplayOverscan.onOverscanCanceled = function() { + DisplayOverscan.getInstance().onOverscanCanceled_(); + }; + + // Export + return { + DisplayOverscan: DisplayOverscan + }; +}); diff --git a/chromium/chrome/browser/resources/options/chromeos/internet_detail.css b/chromium/chrome/browser/resources/options/chromeos/internet_detail.css new file mode 100644 index 00000000000..a027a56259f --- /dev/null +++ b/chromium/chrome/browser/resources/options/chromeos/internet_detail.css @@ -0,0 +1,109 @@ +/* Copyright (c) 2012 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ + +/* Force tab strip to extend to the left and right edges of the window. */ +#internet-details-content-area { + -webkit-box-orient: vertical; + display: -webkit-box; + padding: 6px 0 0 0; +} + +#network-details-header { + -webkit-padding-start: 20px; + margin: 0; + padding-bottom: 12px; + padding-top: 32px; +} + +#network-details-title { + font-size: 18px; +} + +#network-details-subtitle-status { + color: rgb(53, 174, 71); +} + + +/* Fix the height of the subpages so that the dialog does not resize when the + user switches tabs. */ +#internet-details-content-area > .subpages-tab-contents { + -webkit-box-flex: 1; + -webkit-box-sizing: border-box; + -webkit-padding-end: 10px; + height: 390px; + min-width: 480px; + overflow-y: auto; +} + +/* Avoid additional margins between text fields and controlled setting + indicators as the fields in these dialogs have sufficient spacing around + them already. */ +#internet-details-content-area + input:-webkit-any([type='text'],[type='url'],:not([type])) + + .controlled-setting-indicator { + -webkit-margin-start: 0; +} + +#vpn-tab td { + padding: 0; +} + +#vpn-tab .option-value:not(input) { + padding: 4px; +} + +#ip-config-list { + min-height: 96px !important; +} + +/* Minimum and maximum height are integer multiples of the height of a list + entry. */ +#ignored-host-list { + -webkit-margin-start: 0; + border: 1px solid #bfbfbf; + min-height: 64px; + width: 400px; +} + +#ignored-host-list[disabled] { + background-color: rgb(235, 235, 228); + color: #999; + opacity: 1; +} + +#new-host { + -webkit-margin-start: 0; + margin-top: 8px; +} + +#ipconfig-section { + border-top: 1px solid #eee; + margin-bottom: 8px; + padding-top: 8px; +} + +#ipconfig-dns-section { + border-top: 1px solid #eee; + padding-top: 8px; +} + +#user-dns-settings:not([selected]) { + display: none; +} + +.dns-display { + -webkit-margin-start: 24px; + -webkit-transition: opacity 150ms ease-in-out; + color: #bbb; + font-style: italic; +} + +.dns-display:not([selected]) { + -webkit-transition: opacity 150ms ease-in-out; + display: none; +} + +.proxy-subsection { + padding-left: 24px; +} diff --git a/chromium/chrome/browser/resources/options/chromeos/internet_detail.html b/chromium/chrome/browser/resources/options/chromeos/internet_detail.html new file mode 100644 index 00000000000..556c70adb51 --- /dev/null +++ b/chromium/chrome/browser/resources/options/chromeos/internet_detail.html @@ -0,0 +1,708 @@ +<div id="details-internet-page" class="page" hidden> + <div class="close-button"></div> + <!-- Network header --> + <div id="network-details-header"> + <div id="network-details-title"></div> + <div id="network-details-subtitle"> + <span id="network-details-subtitle-status"></span> + <span id="network-details-subtitle-separator"> - </span> + <span id="network-details-subtitle-type"></span> + </div> + </div> + <div id="internet-details-content-area" class="content-area"> + <!-- Navigation tabs --> + <div id="details-tab-strip" class="subpages-nav-tabs"> + <span id="wifi-network-nav-tab" class="tab wifi-details" + tab-contents="wifi-network-tab"> + <span class="tab-label" + i18n-content="wifiNetworkTabLabel"></span> + <span class="active-tab-label" + i18n-content="wifiNetworkTabLabel"></span> + </span> + <span id="vpn-nav-tab" class="tab vpn-details" + tab-contents="vpn-tab"> + <span class="tab-label" + i18n-content="vpnTabLabel"></span> + <span class="active-tab-label" i18n-content="vpnTabLabel"></span> + </span> + <span id="wimax-network-nav-tab" class="tab wimax-details" + tab-contents="wimax-network-tab"> + <span class="tab-label" + i18n-content="wimaxConnTabLabel"></span> + <span class="active-tab-label" + i18n-content="wimaxConnTabLabel"></span> + </span> + <span id="cellular-conn-nav-tab" class="tab cellular-details" + tab-contents="cellular-conn-tab"> + <span class="tab-label" + i18n-content="cellularConnTabLabel"></span> + <span class="active-tab-label" + i18n-content="cellularConnTabLabel"></span> + </span> + <span id="cellular-device-nav-tab" class="tab cellular-details" + tab-contents="cellular-device-tab"> + <span class="tab-label" + i18n-content="cellularDeviceTabLabel"></span> + <span class="active-tab-label" + i18n-content="cellularDeviceTabLabel"></span> + </span> + <span id="internet-nav-tab" class="tab network-details" + tab-contents="internet-tab"> + <span class="tab-label" i18n-content="networkTabLabel"></span> + <span class="active-tab-label" i18n-content="networkTabLabel"></span> + </span> + <span id="security-nav-tab" class="tab cellular-details gsm-only" + tab-contents="security-tab"> + <span class="tab-label" i18n-content="securityTabLabel"></span> + <span class="active-tab-label" i18n-content="securityTabLabel"></span> + </span> + <span id="internet-proxy-nav-tab" class="tab proxy-details" + tab-contents="network-proxy-tab"> + <span class="tab-label" i18n-content="proxyTabLabel"></span> + <span class="active-tab-label" i18n-content="proxyTabLabel"></span> + </span> + </div> + <div id="wifi-network-tab" class="subpages-tab-contents wifi-details"> + <section> + <table class="option-control-table"> + <tr id="prefer-network"> + <td> + <div class="checkbox"> + <span class="controlled-setting-with-label"> + <input id="prefer-network-wifi" type="checkbox"> + <span> + <label for="prefer-network-wifi" + i18n-content="inetPreferredNetwork"> + </label> + <span class="controlled-setting-indicator" data="preferred" + for="prefer-network-wifi"> + </span> + </span> + </span> + </div> + </td> + </tr> + <tr class="auto-connect-network"> + <td> + <div class="checkbox"> + <span class="controlled-setting-with-label"> + <input id="auto-connect-network-wifi" type="checkbox"> + <span> + <label for="auto-connect-network-wifi" + i18n-content="inetAutoConnectNetwork"> + </label> + <span class="controlled-setting-indicator" + data="autoConnect" for="auto-connect-network-wifi"> + </span> + </span> + </span> + </div> + </td> + </tr> + </table> + </section> + <section> + <table id="wifi-settings-table"> + <tr> + <td class="option-name" i18n-content="connectionState"></td> + <td id="wifi-connection-state" class="option-value"></td> + <tr> + <td class="option-name" i18n-content="inetSsid"></td> + <td id="wifi-ssid" class="option-value"></td> + </tr> + <tr id="wifi-bssid-entry"> + <td class="option-name" i18n-content="inetBssid"></td> + <td id="wifi-bssid" class="option-value"></td> + </tr> + <tr class="wifi-network-setting"> + <td class="option-name" i18n-content="inetAddress"></td> + <td id="wifi-ip-address" class="option-value"></td> + </tr> + <tr class="wifi-network-setting"> + <td class="option-name" i18n-content="inetNetmask"></td> + <td id="wifi-netmask" class="option-value"></td> + </tr> + <tr class="wifi-network-setting"> + <td class="option-name" i18n-content="inetGateway"></td> + <td id="wifi-gateway" class="option-value"></td> + </tr> + <tr class="wifi-network-setting"> + <td class="option-name" i18n-content="inetNameServers"></td> + <td id="wifi-name-servers" class="option-value"></td> + </tr> + <tr id="wifi-security-entry"> + <td class="options-name" i18n-content="inetEncryption"></td> + <td id="wifi-security" class="option-value"></td> + </tr> + <tr> + <td class="options-name" i18n-content="inetFrequency"></td> + <td id="wifi-frequency" class="option-value"></td> + </tr> + <tr> + <td class="options-name" i18n-content="inetSignalStrength"></td> + <td id="wifi-signal-strength" class="option-value"></td> + </tr> + <tr id="wifi-hardware-address-entry"> + <td class="option-name" i18n-content="hardwareAddress"></td> + <td id="wifi-hardware-address" class="option-value"></td> + </tr> + </table> + </section> + <section> + <table class="option-control-table"> + <tr> + <td id="password-details" class="option-name" + i18n-content="inetPassProtected"> + </td> + </tr> + <tr> + <td id="wifi-shared-network" class="option-name" + i18n-content="inetNetworkShared"> + </td> + </tr> + </table> + </section> + </div> + <div id="wimax-network-tab" class="subpages-tab-contents wimax-details"> + <section> + <table class="option-control-table"> + <tr class="auto-connect-network"> + <td> + <div class="checkbox"> + <span class="controlled-setting-with-label"> + <input id="auto-connect-network-wimax" type="checkbox"> + <span> + <label for="auto-connect-network-wimax" + i18n-content="inetAutoConnectNetwork"> + </label> + <span class="controlled-setting-indicator" + data="autoConnect" for="auto-connect-network-wimax"> + </span> + </span> + </span> + </div> + </td> + </tr> + </table> + </section> + <section> + <table id="wimax-settings-table"> + <tr> + <td class="option-name" i18n-content="connectionState"></td> + <td id="wimax-connection-state" class="option-value"></td> + </tr> + <tr id="wimax-eap-identity-entry"> + <td class="option-name" i18n-content="inetUsername"></td> + <td id="wimax-eap-identity" class="option-value"></td> + </tr> + <tr> + <td class="options-name" i18n-content="inetSignalStrength"></td> + <td id="wimax-signal-strength" class="option-value"></td> + </tr> + </table> + </section> + <section> + <table class="option-control-table"> + <tr> + <td id="wimax-shared-network" class="option-name" + i18n-content="inetNetworkShared"> + </td> + </tr> + </table> + </section> + </div> + <div id="vpn-tab" class="subpages-tab-contents vpn-details"> + <section> + <table class="option-control-table"> + <tr class="auto-connect-network"> + <td> + <div class="checkbox"> + <span class="controlled-setting-with-label"> + <input id="auto-connect-network-vpn" type="checkbox"> + <span> + <label for="auto-connect-network-vpn" + i18n-content="inetAutoConnectNetwork"> + </label> + <span class="controlled-setting-indicator" + data="autoConnect" for="auto-connect-network-vpn"> + </span> + </span> + </span> + </div> + </td> + </tr> + <tr> + <td class="option-name" i18n-content="inetServiceName"></td> + <td id="inet-service-name" class="option-value"></td> + </tr> + <tr> + <td class="option-name" i18n-content="inetServerHostname"></td> + <td> + <input class="option-value" id="inet-server-hostname"></input> + <span class="controlled-setting-indicator" + data="serverHostname" for="inet-server-hostname"></span> + </td> + </tr> + <tr> + <td class="option-name" i18n-content="inetProviderType"></td> + <td id="inet-provider-type" class="option-value"></td> + </tr> + <tr> + <td class="option-name" i18n-content="inetUsername"></td> + <td id="inet-username" class="option-value"></td> + </tr> + </table> + </section> + </div> + <div id="cellular-conn-tab" class="subpages-tab-contents cellular-details"> + <section id="cellular-network-options"> + <table class="option-control-table"> + <tr> + <td class="option-name" i18n-content="serviceName"></td> + <td id="service-name" class="option-value"> + <select id="select-carrier"> + </select> + <div id="switch-carrier-spinner" class="inline-spinner" hidden> + </div> + </td> + </tr> + <tr> + <td class="option-name" i18n-content="networkTechnology"></td> + <td id="network-technology" class="option-value"></td> + </tr> + <tr> + <td class="option-name" i18n-content="activationState"></td> + <td id="activation-state" class="option-value"></td> + </tr> + <tr> + <td class="option-name" i18n-content="roamingState"></td> + <td id="roaming-state" class="option-value"></td> + </tr> + <tr> + <td class="option-name" i18n-content="restrictedPool"></td> + <td id="restricted-pool" class="option-value"></td> + </tr> + <tr> + <td class="option-name" i18n-content="operatorName"></td> + <td id="operator-name" class="option-value"></td> + </tr> + <tr> + <td class="option-name" i18n-content="operatorCode"></td> + <td id="operator-code" class="option-value"></td> + </tr> + <tr> + <td class="option-name" i18n-content="errorState"></td> + <td id="error-state" class="option-value"></td> + </tr> + <tr class="gsm-only apn-list-view"> + <td class="option-name" i18n-content="cellularApnLabel"></td> + <td id="cellular-apn-label" class="option-value"> + <select id="select-apn"> + <option value="-1" i18n-content="cellularApnOther"> + </option> + </select> + <span class="controlled-setting-indicator" data="providerApnList" + for="select-apn"></span> + </td> + </tr> + <tr class="gsm-only apn-details-view"> + <td class="option-name" i18n-content="cellularApnLabel"></td> + <td id="cellular-apn-label" class="option-value"> + <input id="cellular-apn" type="text"> + </td> + </tr> + <tr class="gsm-only apn-details-view"> + <td class="option-name" i18n-content="cellularApnUsername"></td> + <td id="cellular-apn-username-label" class="option-value"> + <input id="cellular-apn-username" type="text"> + </td> + </tr> + <tr class="gsm-only apn-details-view"> + <td class="option-name" i18n-content="cellularApnPassword"></td> + <td id="cellular-apn-password-label" class="option-value"> + <input id="cellular-apn-password" type="password"> + </td> + </tr> + <tr class="gsm-only apn-details-view"> + <td class="option-name"></td> + <td class="option-value"> + <button id="cellular-apn-use-default" + i18n-content="cellularApnUseDefault"></button> + <button id="cellular-apn-set" + i18n-content="cellularApnSet"></button> + <button id="cellular-apn-cancel" + i18n-content="cellularApnCancel"></button> + </td> + </tr> + <tr> + <td colspan="2"> + <div class="checkbox"> + <span class="controlled-setting-with-label"> + <input id="auto-connect-network-cellular" type="checkbox"> + <span> + <label for="auto-connect-network-cellular" + i18n-content="inetAutoConnectNetwork"> + </label> + <span class="controlled-setting-indicator" + data="autoConnect" for="auto-connect-network-cellular"> + </span> + </span> + </span> + </div> + </td> + </tr> + </table> + </section> + </div> + <div id="cellular-device-tab" class="subpages-tab-contents + cellular-details"> + <section id="cellular-device-options"> + <table class="option-control-table"> + <tr> + <td class="option-name" i18n-content="manufacturer"></td> + <td id="manufacturer" class="option-value"></td> + </tr> + <tr> + <td class="option-name" i18n-content="modelId"></td> + <td id="model-id" class="option-value"></td> + </tr> + <tr> + <td class="option-name" i18n-content="firmwareRevision"></td> + <td id="firmware-revision" class="option-value"></td> + </tr> + <tr> + <td class="option-name" i18n-content="hardwareRevision"></td> + <td id="hardware-revision" class="option-value"></td> + </tr> + <tr class="cdma-only"> + <td class="option-name" i18n-content="prlVersion"></td> + <td id="prl-version" class="option-value"></td> + </tr> + <tr> + <td class="option-name">MEID:</td> + <td id="meid" class="option-value"></td> + </tr> + <tr class="gsm-only"> + <td class="option-name">ICCID:</td> + <td id="iccid" class="option-value"></td> + </tr> + <tr> + <td class="option-name">ESN:</td> + <td id="esn" class="option-value"></td> + </tr> + <tr> + <td class="option-name">IMEI:</td> + <td id="imei" class="option-value"></td> + </tr> + <tr class="gsm-only"> + <td class="option-name">IMSI:</td> + <td id="imsi" class="option-value"></td> + </tr> + <tr> + <td class="option-name">MDN:</td> + <td id="mdn" class="option-value"></td> + </tr> + <tr> + <td class="option-name">MIN/MSID:</td> + <td id="min" class="option-value"></td> + </tr> + </table> + </section> + </div> + <div id="internet-tab" class="subpages-tab-contents"> + <section id="advanced-section"> + <table class="option-control-table"> + <tr> + <td class="option-name" i18n-content="connectionState"></td> + <td id="connection-state" class="option-value"></td> + </tr> + <tr id="hardware-address-row"> + <td class="option-name" i18n-content="hardwareAddress"></td> + <td id="hardware-address" class="option-value"></td> + </tr> + </table> + </section> + <section id="ipconfig-section"> + <div id="ip-automatic-configuration" class="checkbox"> + <span class="controlled-setting-with-label"> + <input id="ip-automatic-configuration-checkbox" + type="checkbox"> + <span> + <label for="ip-automatic-configuration-checkbox" + i18n-content="ipAutomaticConfiguration"> + </label> + <span class="controlled-setting-indicator" data="ipconfigDHCP" + for="ip-automatic-configuration"> + </span> + </span> + </span> + </div> + <div> + <table id="ip-address-settings"> + <tr> + <td class="spacer" width="14px"></td> + <td class="option-name" i18n-content="inetAddress"></td> + <td><div id="ip-address"></div></td> + </tr> + <tr> + <td class="spacer" width="14px"></td> + <td class="option-name" id="ip-netmask-label" + i18n-content="inetNetmask"></td> + <td><div id="ip-netmask"></div></td> + </tr> + <tr> + <td class="spacer" width="14px"></td> + <td class="option-name" i18n-content="inetGateway"></td> + <td><div id="ip-gateway"></div></td> + </tr> + </table> + </section> + <section id="ipconfig-dns-section"> + <div class="radio"> + <label> + <input id="automatic-dns-radio" type="radio" name="dnstype" + value="automatic"> + <span i18n-content="automaticNameServers"></span> + </label> + </div> + <div id="automatic-dns-display" class="dns-display"></div> + <div class="radio"> + <label> + <input id="google-dns-radio" type="radio" name="dnstype" + value="google"> + <span id="google-dns-label"></span> + </label> + </div> + <div id="google-dns-display" class="dns-display"></div> + <div class="radio"> + <label> + <input id="user-dns-radio" type="radio" name="dnstype" + value="user"> + <span i18n-content="userNameServers"></span> + </label> + </div> + <table id="user-dns-settings"> + <tr> + <td class="spacer" width="14px"></td> + <td> + <div id="ipconfig-dns1" i18n-placeholder-text="userNameServer1" + allow-empty> + </div> + </td> + <td> + <div id="ipconfig-dns2" i18n-placeholder-text="userNameServer2" + allow-empty> + </div> + </td> + </tr> + <tr> + <td class="spacer" width="14px"></td> + <td> + <div id="ipconfig-dns3" i18n-placeholder-text="userNameServer3" + allow-empty> + </div> + </td> + <td> + <div id="ipconfig-dns4" i18n-placeholder-text="userNameServer4" + allow-empty> + </div> + </td> + </tr> + </table> + </section> + </div> + <div id="security-tab" + class="subpages-tab-contents cellular-details gsm-only"> + <div id="cellular-security-options"> + <section> + <div id="sim-pin-lock" class="checkbox"> + <span class="controlled-setting-with-label"> + <input id="sim-card-lock-enabled" type="checkbox"> + <span> + <label for="sim-card-lock-enabled" i18n-content="lockSimCard"> + </label> + <span class="controlled-setting-indicator" + data="simCardLockEnabled" for="sim-card-lock-enabled"> + </span> + </span> + </span> + </div> + </section> + <section> + <div id="change-pin-area"> + <button id="change-pin" i18n-content="changePinButton"></button> + <span class="controlled-setting-indicator" data="simCardLockEnabled" + for="change-pin"></span> + </div> + </section> + </div> + </div> + <div id="network-proxy-tab" class="subpages-tab-contents"> + <section> + <div id="network-proxy-info-banner" hidden> + <span id="banner-text" class="page-banner-text"></span> + </div> + <div class="radio"> + <label> + <input id="direct-proxy" type="radio" name="proxytype" value="1" + pref="cros.session.proxy.type"> + <span i18n-content="proxyDirectInternetConnection"></span> + </label> + </div> + <div class="radio"> + <label> + <input id="auto-proxy" type="radio" name="proxytype" value="3" + pref="cros.session.proxy.type"> + <span i18n-content="proxyAutomatic"></span> + </label> + </div> + <div class="proxy-subsection" id="auto-proxy-parms"> + <div class="checkbox"> + <label> + <input id="proxy-use-pac-url" type="checkbox" + pref="cros.session.proxy.usepacurl"> + <span i18n-content="proxyUseConfigUrl"></span> + </label> + </div> + <div> + <label> + <input id="proxy-pac-url" type="url" size="50" + pref="cros.session.proxy.pacurl"> + </label> + </div> + </div> + <div class="radio"> + <label> + <input id="manual-proxy" type="radio" name="proxytype" value="2" + pref="cros.session.proxy.type"> + <span i18n-content="proxyManual"></span> + </label> + </div> + <div class="proxy-subsection" id="manual-proxy-parms"> + <div class="checkbox"> + <label> + <input id="proxy-all-protocols" type="checkbox" + pref="cros.session.proxy.single"> + <span i18n-content="sameProxyProtocols"></span> + </label> + </div> + <div id="single-proxy"> + <table> + <tr> + <td> + <span i18n-content="httpProxy"></span> + <input id="proxy-host-single-name" type="text" size="25" + pref="cros.session.proxy.singlehttp" disabled> + </td> + <td> + <span i18n-content="proxyPort"></span> + <input id="proxy-host-single-port" size="4" + pref="cros.session.proxy.singlehttpport" disabled> + </td> + </tr> + </table> + </div> + <div id="multi-proxy"> + <table> + <tr> + <td> + <span i18n-content="httpProxy"></span> + </td> + <td> + <input id="proxy-host-name" type="text" size="25" + pref="cros.session.proxy.httpurl" disabled> + </td> + <td> + <span i18n-content="proxyPort"></span> + </td> + <td> + <input id="proxy-host-port" size="4" + pref="cros.session.proxy.httpport" disabled> + </td> + </tr> + <tr> + <td> + <span i18n-content="secureHttpProxy"></span> + </td> + <td> + <input id="secure-proxy-host-name" type="text" size="25" + pref="cros.session.proxy.httpsurl" disabled> + </td> + <td> + <span i18n-content="proxyPort"></span> + </td> + <td> + <input id="secure-proxy-port" size="4" + pref="cros.session.proxy.httpsport" disabled> + </td> + </tr> + <tr> + <td> + <span i18n-content="ftpProxy"></span> + </td> + <td> + <input id="ftp-proxy" type="text" size="25" + pref="cros.session.proxy.ftpurl" disabled> + </td> + <td> + <span i18n-content="proxyPort"></span> + </td> + <td> + <input id="ftp-proxy-port" size="4" + pref="cros.session.proxy.ftpport" disabled> + </td> + </tr> + <tr> + <td> + <span i18n-content="socksHost"></span> + </td> + <td> + <input id="socks-host" type="text" size="25" + pref="cros.session.proxy.socks" disabled> + </td> + <td> + <span i18n-content="proxyPort"></span> + </td> + <td> + <input id="socks-port" size="4" + pref="cros.session.proxy.socksport" disabled> + </td> + </tr> + </table> + </div> + <div id="advanced-config"> + <div class="option vbox flex"> + <div i18n-content="proxyBypass"></div> + <list id="ignored-host-list"></list> + <input id="new-host" type="url" size="30"> + <button id="add-host" i18n-content="addHost"></button> + <button id="remove-host" i18n-content="removeHost"></button> + </div> + </div> + </div> + <div class="proxy-subsection" id="web-proxy-auto-discovery"> + <span i18n-content="webProxyAutoDiscoveryUrl"></span> + <input id="web-proxy-auto-discovery-url" type="url" disabled> + </div> + </section> + </div> + </div> + <div class="action-area"> + <div class="button-strip"> + <!-- TODO(dbeam): Clarify style guide regarding tag wrap. --> + <button id="details-internet-dismiss" class="default-button" + i18n-content="detailsInternetDismiss"> + </button> + <button id="details-internet-login" i18n-content="connectButton"> + </button> + <button id="details-internet-disconnect" i18n-content="disconnectButton"> + </button> + <button id="details-internet-configure" i18n-content="configureButton"> + </button> + <button id="activate-details" i18n-content="activateButton"></button> + <button id="buyplan-details" i18n-content="buyplanButton"></button> + <button id="view-account-details" i18n-content="viewAccountButton"> + </button> + </div> + </div> +</div> diff --git a/chromium/chrome/browser/resources/options/chromeos/internet_detail.js b/chromium/chrome/browser/resources/options/chromeos/internet_detail.js new file mode 100644 index 00000000000..cbc07f21d2c --- /dev/null +++ b/chromium/chrome/browser/resources/options/chromeos/internet_detail.js @@ -0,0 +1,1265 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options.internet', function() { + var OptionsPage = options.OptionsPage; + /** @const */ var ArrayDataModel = cr.ui.ArrayDataModel; + /** @const */ var IPAddressField = options.internet.IPAddressField; + + /** + * Network settings constants. These enums must match their C++ + * counterparts. + */ + function Constants() {} + + // Network types: + Constants.TYPE_UNKNOWN = 'UNKNOWN'; + Constants.TYPE_ETHERNET = 'ethernet'; + Constants.TYPE_WIFI = 'wifi'; + Constants.TYPE_WIMAX = 'wimax'; + Constants.TYPE_BLUETOOTH = 'bluetooth'; + Constants.TYPE_CELLULAR = 'cellular'; + Constants.TYPE_VPN = 'vpn'; + + /* + * Helper function to set hidden attribute for elements matching a selector. + * @param {string} selector CSS selector for extracting a list of elements. + * @param {bool} hidden New hidden value. + */ + function updateHidden(selector, hidden) { + var elements = cr.doc.querySelectorAll(selector); + for (var i = 0, el; el = elements[i]; i++) { + el.hidden = hidden; + } + } + + /* + * Helper function to update the properties of the data object from the + * properties in the update object. + * @param {object} data object to update. + * @param {object} object containing the updated properties. + */ + function updateDataObject(data, update) { + for (prop in update) { + if (prop in data) + data[prop] = update[prop]; + } + } + + /** + * Monitor pref change of given element. + * @param {Element} el Target element. + */ + function observePrefsUI(el) { + Preferences.getInstance().addEventListener(el.pref, handlePrefUpdate); + } + + /** + * UI pref change handler. + * @param {Event} e The update event. + */ + function handlePrefUpdate(e) { + DetailsInternetPage.getInstance().updateControls(); + } + + /** + * Simple helper method for converting a field to a string. It is used to + * easily assign an empty string from fields that may be unknown or undefined. + * @param {object} value that should be converted to a string. + * @return {string} the result. + */ + function stringFromValue(value) { + return value ? String(value) : ''; + } + + /** + * Sends the 'checked' state of a control to chrome for a network. + * @param {string} path The service path of the network. + * @param {string} message The message to send to chrome. + * @param {HTMLInputElement} checkbox The checkbox storing the value to send. + */ + function sendCheckedIfEnabled(path, message, checkbox) { + if (!checkbox.hidden && !checkbox.disabled) + chrome.send(message, [path, checkbox.checked ? 'true' : 'false']); + } + + ///////////////////////////////////////////////////////////////////////////// + // DetailsInternetPage class: + + /** + * Encapsulated handling of ChromeOS internet details overlay page. + * @constructor + */ + function DetailsInternetPage() { + OptionsPage.call(this, + 'detailsInternetPage', + null, + 'details-internet-page'); + } + + cr.addSingletonGetter(DetailsInternetPage); + + DetailsInternetPage.prototype = { + __proto__: OptionsPage.prototype, + + /** + * Initializes DetailsInternetPage page. + * Calls base class implementation to starts preference initialization. + */ + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + var params = parseQueryParams(window.location); + this.initializePageContents_(params); + this.showNetworkDetails_(params); + }, + + /** + * Auto-activates the network details dialog if network information + * is included in the URL. + */ + showNetworkDetails_: function(params) { + var servicePath = params.servicePath; + if (!servicePath || !servicePath.length) + return; + var networkType = ''; // ignored for 'options' + chrome.send('networkCommand', [networkType, servicePath, 'options']); + }, + + + /** + * Initializes the contents of the page. + */ + initializePageContents_: function(params) { + $('details-internet-dismiss').addEventListener('click', function(event) { + DetailsInternetPage.setDetails(); + }); + + $('details-internet-login').addEventListener('click', function(event) { + DetailsInternetPage.setDetails(); + DetailsInternetPage.loginFromDetails(); + }); + + $('details-internet-disconnect').addEventListener('click', + function(event) { + DetailsInternetPage.setDetails(); + DetailsInternetPage.disconnectNetwork(); + }); + + $('details-internet-configure').addEventListener('click', + function(event) { + DetailsInternetPage.setDetails(); + DetailsInternetPage.configureNetwork(); + }); + + $('activate-details').addEventListener('click', function(event) { + DetailsInternetPage.activateFromDetails(); + }); + + $('buyplan-details').addEventListener('click', function(event) { + var data = $('connection-state').data; + chrome.send('buyDataPlan', [data.servicePath]); + OptionsPage.closeOverlay(); + }); + + $('view-account-details').addEventListener('click', function(event) { + var data = $('connection-state').data; + chrome.send('showMorePlanInfo', [data.servicePath]); + OptionsPage.closeOverlay(); + }); + + $('cellular-apn-use-default').addEventListener('click', function(event) { + var data = $('connection-state').data; + var apnSelector = $('select-apn'); + + if (data.userApnIndex != -1) { + apnSelector.remove(data.userApnIndex); + data.userApnIndex = -1; + } + + if (data.providerApnList.value.length > 0) { + var iApn = 0; + var defaultApn = data.providerApnList.value[iApn]; + data.apn.apn = stringFromValue(defaultApn.apn); + data.apn.username = stringFromValue(defaultApn.username); + data.apn.password = stringFromValue(defaultApn.password); + chrome.send('setApn', [data.servicePath, + data.apn.apn, + data.apn.username, + data.apn.password]); + apnSelector.selectedIndex = iApn; + data.selectedApn = iApn; + } else { + data.apn.apn = ''; + data.apn.username = ''; + data.apn.password = ''; + apnSelector.selectedIndex = -1; + data.selectedApn = -1; + } + updateHidden('.apn-list-view', false); + updateHidden('.apn-details-view', true); + }); + + $('cellular-apn-set').addEventListener('click', function(event) { + if ($('cellular-apn').value == '') + return; + + var data = $('connection-state').data; + var apnSelector = $('select-apn'); + + data.apn.apn = stringFromValue($('cellular-apn').value); + data.apn.username = stringFromValue($('cellular-apn-username').value); + data.apn.password = stringFromValue($('cellular-apn-password').value); + data.userApn = { + 'apn': data.apn.apn, + 'username': data.apn.username, + 'password': data.apn.password + }; + chrome.send('setApn', [data.servicePath, + data.apn.apn, + data.apn.username, + data.apn.password]); + + if (data.userApnIndex != -1) { + apnSelector.remove(data.userApnIndex); + data.userApnIndex = -1; + } + + var option = document.createElement('option'); + option.textContent = data.apn.apn; + option.value = -1; + option.selected = true; + apnSelector.add(option, apnSelector[apnSelector.length - 1]); + data.userApnIndex = apnSelector.length - 2; + data.selectedApn = data.userApnIndex; + + updateHidden('.apn-list-view', false); + updateHidden('.apn-details-view', true); + }); + + $('cellular-apn-cancel').addEventListener('click', function(event) { + $('select-apn').selectedIndex = $('connection-state').data.selectedApn; + updateHidden('.apn-list-view', false); + updateHidden('.apn-details-view', true); + }); + + $('select-apn').addEventListener('change', function(event) { + var data = $('connection-state').data; + var apnSelector = $('select-apn'); + if (apnSelector[apnSelector.selectedIndex].value != -1) { + var apnList = data.providerApnList.value; + chrome.send('setApn', [data.servicePath, + stringFromValue(apnList[apnSelector.selectedIndex].apn), + stringFromValue(apnList[apnSelector.selectedIndex].username), + stringFromValue(apnList[apnSelector.selectedIndex].password)] + ); + data.selectedApn = apnSelector.selectedIndex; + } else if (apnSelector.selectedIndex == data.userApnIndex) { + chrome.send('setApn', [data.servicePath, + stringFromValue(data.userApn.apn), + stringFromValue(data.userApn.username), + stringFromValue(data.userApn.password)]); + data.selectedApn = apnSelector.selectedIndex; + } else { + $('cellular-apn').value = stringFromValue(data.apn.apn); + $('cellular-apn-username').value = stringFromValue(data.apn.username); + $('cellular-apn-password').value = stringFromValue(data.apn.password); + + updateHidden('.apn-list-view', true); + updateHidden('.apn-details-view', false); + } + }); + + $('sim-card-lock-enabled').addEventListener('click', function(event) { + var newValue = $('sim-card-lock-enabled').checked; + // Leave value as is because user needs to enter PIN code first. + // When PIN will be entered and value changed, + // we'll update UI to reflect that change. + $('sim-card-lock-enabled').checked = !newValue; + chrome.send('setSimCardLock', [newValue]); + }); + $('change-pin').addEventListener('click', function(event) { + chrome.send('changePin'); + }); + + // Proxy + ['proxy-host-single-port', + 'secure-proxy-port', + 'socks-port', + 'ftp-proxy-port', + 'proxy-host-port' + ].forEach(function(id) { + options.PrefPortNumber.decorate($(id)); + }); + + options.proxyexceptions.ProxyExceptions.decorate($('ignored-host-list')); + $('remove-host').addEventListener('click', + this.handleRemoveProxyExceptions_); + $('add-host').addEventListener('click', this.handleAddProxyException_); + $('direct-proxy').addEventListener('click', this.disableManualProxy_); + $('manual-proxy').addEventListener('click', this.enableManualProxy_); + $('auto-proxy').addEventListener('click', this.disableManualProxy_); + $('proxy-all-protocols').addEventListener('click', + this.toggleSingleProxy_); + $('proxy-use-pac-url').addEventListener('change', + this.handleAutoConfigProxy_); + + observePrefsUI($('direct-proxy')); + observePrefsUI($('manual-proxy')); + observePrefsUI($('auto-proxy')); + observePrefsUI($('proxy-all-protocols')); + observePrefsUI($('proxy-use-pac-url')); + + $('ip-automatic-configuration-checkbox').addEventListener('click', + this.handleIpAutoConfig_); + $('automatic-dns-radio').addEventListener('click', + this.handleNameServerTypeChange_); + $('google-dns-radio').addEventListener('click', + this.handleNameServerTypeChange_); + $('user-dns-radio').addEventListener('click', + this.handleNameServerTypeChange_); + + // We only load this string if we have the string data available + // because the proxy settings page on the login screen re-uses the + // proxy sub-page from the internet options, and it doesn't ever + // show the DNS settings, so we don't need this string there. + // The string isn't available because + // chrome://settings-frame/strings.js (where the string is + // stored) is not accessible from the login screen. + // TODO(pneubeck): Remove this once i18n of the proxy dialog on the login + // page is fixed. http://crbug.com/242865 + if (loadTimeData.data_) { + $('google-dns-label').innerHTML = + loadTimeData.getString('googleNameServers'); + } + }, + + /** + * Handler for "add" event fired from userNameEdit. + * @param {Event} e Add event fired from userNameEdit. + * @private + */ + handleAddProxyException_: function(e) { + var exception = $('new-host').value; + $('new-host').value = ''; + + exception = exception.trim(); + if (exception) + $('ignored-host-list').addException(exception); + }, + + /** + * Handler for when the remove button is clicked + * @param {Event} e The click event. + * @private + */ + handleRemoveProxyExceptions_: function(e) { + var selectedItems = $('ignored-host-list').selectedItems; + for (var x = 0; x < selectedItems.length; x++) { + $('ignored-host-list').removeException(selectedItems[x]); + } + }, + + /** + * Handler for when the IP automatic configuration checkbox is clicked. + * @param {Event} e The click event. + * @private + */ + handleIpAutoConfig_: function(e) { + var checked = $('ip-automatic-configuration-checkbox').checked; + var fields = [$('ip-address'), $('ip-netmask'), $('ip-gateway')]; + for (var i = 0; i < fields.length; ++i) { + fields[i].editable = !checked; + if (checked) { + var model = fields[i].model; + model.value = model.automatic; + fields[i].model = model; + } + } + if (!checked) + $('ip-address').focus(); + }, + + /** + * Handler for when the name server selection changes. + * @param {Event} e The click event. + * @private + */ + handleNameServerTypeChange_: function(event) { + var type = event.target.value; + DetailsInternetPage.updateNameServerDisplay(type); + }, + + /** + * Update details page controls. + * @private + */ + updateControls: function() { + // Only show ipconfig section if network is connected OR if nothing on + // this device is connected. This is so that you can fix the ip configs + // if you can't connect to any network. + // TODO(chocobo): Once ipconfig is moved to flimflam service objects, + // we need to redo this logic to allow configuration of all networks. + $('ipconfig-section').hidden = !this.connected && this.deviceConnected; + $('ipconfig-dns-section').hidden = + !this.connected && this.deviceConnected; + + // Network type related. + updateHidden('#details-internet-page .cellular-details', !this.cellular); + updateHidden('#details-internet-page .wifi-details', !this.wireless); + updateHidden('#details-internet-page .wimax-details', !this.wimax); + updateHidden('#details-internet-page .vpn-details', !this.vpn); + updateHidden('#details-internet-page .proxy-details', !this.showProxy); + // Conditionally call updateHidden on .gsm-only, so that we don't unhide + // a previously hidden element. + if (this.gsm) + updateHidden('#details-internet-page .cdma-only', true); + else + updateHidden('#details-internet-page .gsm-only', true); + /* Network information merged into the Wifi tab for wireless networks + unless the option is set for enabling a static IP configuration. */ + updateHidden('#details-internet-page .network-details', + (this.wireless && !this.showStaticIPConfig) || this.vpn); + updateHidden('#details-internet-page .wifi-network-setting', + this.showStaticIPConfig); + + // Wifi - Password and shared. + updateHidden('#details-internet-page #password-details', + !this.wireless || !this.password); + updateHidden('#details-internet-page #wifi-shared-network', + !this.shared); + updateHidden('#details-internet-page #prefer-network', + !this.showPreferred); + + // WiMAX. + updateHidden('#details-internet-page #wimax-shared-network', + !this.shared); + + // Proxy + this.updateProxyBannerVisibility_(); + this.toggleSingleProxy_(); + if ($('manual-proxy').checked) + this.enableManualProxy_(); + else + this.disableManualProxy_(); + }, + + /** + * Updates info banner visibility state. This function shows the banner + * if proxy is managed or shared-proxies is off for shared network. + * @private + */ + updateProxyBannerVisibility_: function() { + var bannerDiv = $('network-proxy-info-banner'); + if (!loadTimeData.data_) { + // TODO(pneubeck): This temporarily prevents an exception below until + // i18n of the proxy dialog on the login page is + // fixed. http://crbug.com/242865 + bannerDiv.hidden = true; + return; + } + + // Show banner and determine its message if necessary. + var controlledBy = $('direct-proxy').controlledBy; + if (!controlledBy || controlledBy == '') { + bannerDiv.hidden = true; + } else { + bannerDiv.hidden = false; + // The possible banner texts are loaded in proxy_handler.cc. + var bannerText = 'proxyBanner' + controlledBy.charAt(0).toUpperCase() + + controlledBy.slice(1); + $('banner-text').textContent = loadTimeData.getString(bannerText); + } + }, + + /** + * Handler for when the user clicks on the checkbox to allow a + * single proxy usage. + * @private + * @param {Event} e Click Event. + */ + toggleSingleProxy_: function(e) { + if ($('proxy-all-protocols').checked) { + $('multi-proxy').hidden = true; + $('single-proxy').hidden = false; + } else { + $('multi-proxy').hidden = false; + $('single-proxy').hidden = true; + } + }, + + /** + * Handler for when the user clicks on the checkbox to enter + * auto configuration URL. + * @private + * @param {Event} e Click Event. + */ + handleAutoConfigProxy_: function(e) { + $('proxy-pac-url').disabled = !$('proxy-use-pac-url').checked; + }, + + /** + * Handler for selecting a radio button that will disable the manual + * controls. + * @private + * @param {Event} e Click event. + */ + disableManualProxy_: function(e) { + $('ignored-host-list').disabled = true; + $('new-host').disabled = true; + $('remove-host').disabled = true; + $('add-host').disabled = true; + $('proxy-all-protocols').disabled = true; + $('proxy-host-name').disabled = true; + $('proxy-host-port').disabled = true; + $('proxy-host-single-name').disabled = true; + $('proxy-host-single-port').disabled = true; + $('secure-proxy-host-name').disabled = true; + $('secure-proxy-port').disabled = true; + $('ftp-proxy').disabled = true; + $('ftp-proxy-port').disabled = true; + $('socks-host').disabled = true; + $('socks-port').disabled = true; + $('proxy-use-pac-url').disabled = $('auto-proxy').disabled || + !$('auto-proxy').checked; + $('proxy-pac-url').disabled = $('proxy-use-pac-url').disabled || + !$('proxy-use-pac-url').checked; + $('auto-proxy-parms').hidden = !$('auto-proxy').checked; + $('manual-proxy-parms').hidden = !$('manual-proxy').checked; + }, + + /** + * Handler for selecting a radio button that will enable the manual + * controls. + * @private + * @param {Event} e Click event. + */ + enableManualProxy_: function(e) { + $('ignored-host-list').redraw(); + var allDisabled = $('manual-proxy').disabled; + $('ignored-host-list').disabled = allDisabled; + $('new-host').disabled = allDisabled; + $('remove-host').disabled = allDisabled; + $('add-host').disabled = allDisabled; + $('proxy-all-protocols').disabled = allDisabled; + $('proxy-host-name').disabled = allDisabled; + $('proxy-host-port').disabled = allDisabled; + $('proxy-host-single-name').disabled = allDisabled; + $('proxy-host-single-port').disabled = allDisabled; + $('secure-proxy-host-name').disabled = allDisabled; + $('secure-proxy-port').disabled = allDisabled; + $('ftp-proxy').disabled = allDisabled; + $('ftp-proxy-port').disabled = allDisabled; + $('socks-host').disabled = allDisabled; + $('socks-port').disabled = allDisabled; + $('proxy-use-pac-url').disabled = true; + $('proxy-pac-url').disabled = true; + $('auto-proxy-parms').hidden = !$('auto-proxy').checked; + $('manual-proxy-parms').hidden = !$('manual-proxy').checked; + }, + }; + + /** + * Enables or Disables all buttons that provide operations on the cellular + * network. + */ + DetailsInternetPage.changeCellularButtonsState = function(disable) { + var buttonsToDisableList = + new Array('details-internet-login', + 'details-internet-disconnect', + 'details-internet-configure', + 'activate-details', + 'buyplan-details', + 'view-account-details'); + + for (var i = 0; i < buttonsToDisableList.length; ++i) { + button = $(buttonsToDisableList[i]); + button.disabled = disable; + } + }; + + /** + * Shows a spinner while the carrier is changed. + */ + DetailsInternetPage.showCarrierChangeSpinner = function(visible) { + $('switch-carrier-spinner').hidden = !visible; + // Disable any buttons that allow us to operate on cellular networks. + DetailsInternetPage.changeCellularButtonsState(visible); + }; + + /** + * Changes the network carrier. + */ + DetailsInternetPage.handleCarrierChanged = function() { + var carrierSelector = $('select-carrier'); + var carrier = carrierSelector[carrierSelector.selectedIndex].textContent; + DetailsInternetPage.showCarrierChangeSpinner(true); + var data = $('connection-state').data; + chrome.send('setCarrier', [data.servicePath, carrier]); + }; + + /** + * Performs minimal initialization of the InternetDetails dialog in + * preparation for showing proxy-setttings. + */ + DetailsInternetPage.initializeProxySettings = function() { + var detailsPage = DetailsInternetPage.getInstance(); + detailsPage.initializePageContents_(); + }; + + /** + * Displays the InternetDetails dialog with only the proxy settings visible. + */ + DetailsInternetPage.showProxySettings = function() { + var detailsPage = DetailsInternetPage.getInstance(); + $('network-details-header').hidden = true; + $('buyplan-details').hidden = true; + $('activate-details').hidden = true; + $('view-account-details').hidden = true; + $('web-proxy-auto-discovery').hidden = true; + detailsPage.cellular = false; + detailsPage.wireless = false; + detailsPage.vpn = false; + detailsPage.showProxy = true; + updateHidden('#internet-tab', true); + updateHidden('#details-tab-strip', true); + updateHidden('#details-internet-page .action-area', true); + detailsPage.updateControls(); + detailsPage.visible = true; + }; + + /** + * Initializes even handling for keyboard driven flow. + */ + DetailsInternetPage.initializeKeyboardFlow = function() { + keyboard.initializeKeyboardFlow(); + }; + + DetailsInternetPage.updateProxySettings = function(type) { + var proxyHost = null, + proxyPort = null; + + if (type == 'cros.session.proxy.singlehttp') { + proxyHost = 'proxy-host-single-name'; + proxyPort = 'proxy-host-single-port'; + }else if (type == 'cros.session.proxy.httpurl') { + proxyHost = 'proxy-host-name'; + proxyPort = 'proxy-host-port'; + }else if (type == 'cros.session.proxy.httpsurl') { + proxyHost = 'secure-proxy-host-name'; + proxyPort = 'secure-proxy-port'; + }else if (type == 'cros.session.proxy.ftpurl') { + proxyHost = 'ftp-proxy'; + proxyPort = 'ftp-proxy-port'; + }else if (type == 'cros.session.proxy.socks') { + proxyHost = 'socks-host'; + proxyPort = 'socks-port'; + }else { + return; + } + + var hostValue = $(proxyHost).value; + if (hostValue.indexOf(':') !== -1) { + if (hostValue.match(/:/g).length == 1) { + hostValue = hostValue.split(':'); + $(proxyHost).value = hostValue[0]; + $(proxyPort).value = hostValue[1]; + } + } + }; + + DetailsInternetPage.updateCarrier = function() { + DetailsInternetPage.showCarrierChangeSpinner(false); + }; + + DetailsInternetPage.updateSecurityTab = function(requirePin) { + $('sim-card-lock-enabled').checked = requirePin; + $('change-pin').hidden = !requirePin; + }; + + DetailsInternetPage.loginFromDetails = function() { + var data = $('connection-state').data; + var servicePath = data.servicePath; + chrome.send('networkCommand', [String(data.type), + servicePath, + 'connect']); + OptionsPage.closeOverlay(); + }; + + DetailsInternetPage.disconnectNetwork = function() { + var data = $('connection-state').data; + var servicePath = data.servicePath; + chrome.send('networkCommand', [String(data.type), + servicePath, + 'disconnect']); + OptionsPage.closeOverlay(); + }; + + DetailsInternetPage.configureNetwork = function() { + var data = $('connection-state').data; + var servicePath = data.servicePath; + chrome.send('networkCommand', [String(data.type), + servicePath, + 'configure']); + OptionsPage.closeOverlay(); + }; + + DetailsInternetPage.activateFromDetails = function() { + var data = $('connection-state').data; + var servicePath = data.servicePath; + if (data.type == Constants.TYPE_CELLULAR) { + chrome.send('networkCommand', [String(data.type), + servicePath, + 'activate']); + } + OptionsPage.closeOverlay(); + }; + + DetailsInternetPage.setDetails = function() { + var data = $('connection-state').data; + var servicePath = data.servicePath; + if (data.type == Constants.TYPE_WIFI) { + sendCheckedIfEnabled(servicePath, 'setPreferNetwork', + $('prefer-network-wifi')); + sendCheckedIfEnabled(servicePath, 'setAutoConnect', + $('auto-connect-network-wifi')); + } else if (data.type == Constants.TYPE_WIMAX) { + sendCheckedIfEnabled(servicePath, 'setAutoConnect', + $('auto-connect-network-wimax')); + } else if (data.type == Constants.TYPE_CELLULAR) { + sendCheckedIfEnabled(servicePath, 'setAutoConnect', + $('auto-connect-network-cellular')); + } else if (data.type == Constants.TYPE_VPN) { + chrome.send('setServerHostname', + [servicePath, + $('inet-server-hostname').value]); + sendCheckedIfEnabled(servicePath, 'setAutoConnect', + $('auto-connect-network-vpn')); + } + + var nameServerTypes = ['automatic', 'google', 'user']; + var nameServerType = 'automatic'; + for (var i = 0; i < nameServerTypes.length; ++i) { + if ($(nameServerTypes[i] + '-dns-radio').checked) { + nameServerType = nameServerTypes[i]; + break; + } + } + + // Skip any empty values. + var userNameServers = []; + for (var i = 1; i <= 4; ++i) { + var nameServerField = $('ipconfig-dns' + i); + if (nameServerField && nameServerField.model && + nameServerField.model.value) { + userNameServers.push(nameServerField.model.value); + } + } + + userNameServers = userNameServers.join(','); + + chrome.send('setIPConfig', + [servicePath, + Boolean($('ip-automatic-configuration-checkbox').checked), + $('ip-address').model.value || '', + $('ip-netmask').model.value || '', + $('ip-gateway').model.value || '', + nameServerType, + userNameServers]); + OptionsPage.closeOverlay(); + }; + + DetailsInternetPage.updateNameServerDisplay = function(type) { + var editable = type == 'user'; + var fields = [$('ipconfig-dns1'), $('ipconfig-dns2'), + $('ipconfig-dns3'), $('ipconfig-dns4')]; + for (var i = 0; i < fields.length; ++i) { + fields[i].editable = editable; + } + if (editable) + $('ipconfig-dns1').focus(); + + var automaticDns = $('automatic-dns-display'); + var googleDns = $('google-dns-display'); + var userDns = $('user-dns-settings'); + switch (type) { + case 'automatic': + automaticDns.setAttribute('selected', ''); + googleDns.removeAttribute('selected'); + userDns.removeAttribute('selected'); + break; + case 'google': + automaticDns.removeAttribute('selected'); + googleDns.setAttribute('selected', ''); + userDns.removeAttribute('selected'); + break; + case 'user': + automaticDns.removeAttribute('selected'); + googleDns.removeAttribute('selected'); + userDns.setAttribute('selected', ''); + break; + } + }; + + DetailsInternetPage.updateConnectionButtonVisibilty = function(data) { + $('details-internet-login').hidden = data.connected; + $('details-internet-login').disabled = data.disableConnectButton; + + if (!data.connected && + ((data.type == Constants.TYPE_WIFI && data.encryption) || + data.type == Constants.TYPE_WIMAX || + data.type == Constants.TYPE_VPN)) { + $('details-internet-configure').hidden = false; + } else { + $('details-internet-configure').hidden = true; + } + + if (data.type == Constants.TYPE_ETHERNET) + $('details-internet-disconnect').hidden = true; + else + $('details-internet-disconnect').hidden = !data.connected; + }; + + DetailsInternetPage.updateConnectionData = function(update) { + var detailsPage = DetailsInternetPage.getInstance(); + if (!detailsPage.visible) + return; + + var data = $('connection-state').data; + if (!data) + return; + + if (update.servicePath != data.servicePath) + return; + + // Update our cached data object. + updateDataObject(data, update); + + detailsPage.deviceConnected = data.deviceConnected; + detailsPage.connecting = data.connecting; + detailsPage.connected = data.connected; + $('connection-state').textContent = data.connectionState; + + this.updateConnectionButtonVisibilty(data); + + if (data.type == Constants.TYPE_WIFI) { + $('wifi-connection-state').textContent = data.connectionState; + } else if (data.type == Constants.TYPE_WIMAX) { + $('wimax-connection-state').textContent = data.connectionState; + } else if (data.type == Constants.TYPE_CELLULAR) { + $('activation-state').textContent = data.activationState; + + $('buyplan-details').hidden = !data.showBuyButton; + $('view-account-details').hidden = !data.showViewAccountButton; + + $('activate-details').hidden = !data.showActivateButton; + if (data.showActivateButton) + $('details-internet-login').hidden = true; + } + + $('connection-state').data = data; + }; + + DetailsInternetPage.showDetailedInfo = function(data) { + var detailsPage = DetailsInternetPage.getInstance(); + + // Populate header + $('network-details-title').textContent = data.networkName; + var statusKey = data.connected ? 'networkConnected' : + 'networkNotConnected'; + $('network-details-subtitle-status').textContent = + loadTimeData.getString(statusKey); + var typeKey = null; + switch (data.type) { + case Constants.TYPE_ETHERNET: + typeKey = 'ethernetTitle'; + break; + case Constants.TYPE_WIFI: + typeKey = 'wifiTitle'; + break; + case Constants.TYPE_WIMAX: + typeKey = 'wimaxTitle'; + break; + case Constants.TYPE_CELLULAR: + typeKey = 'cellularTitle'; + break; + case Constants.TYPE_VPN: + typeKey = 'vpnTitle'; + break; + } + var typeLabel = $('network-details-subtitle-type'); + var typeSeparator = $('network-details-subtitle-separator'); + if (typeKey) { + typeLabel.textContent = loadTimeData.getString(typeKey); + typeLabel.hidden = false; + typeSeparator.hidden = false; + } else { + typeLabel.hidden = true; + typeSeparator.hidden = true; + } + + // TODO(chocobo): Is this hack to cache the data here reasonable? + // TODO(kevers): Find more appropriate place to cache data. + $('connection-state').data = data; + + $('buyplan-details').hidden = true; + $('activate-details').hidden = true; + $('view-account-details').hidden = true; + + this.updateConnectionButtonVisibilty(data); + + $('web-proxy-auto-discovery').hidden = true; + + detailsPage.deviceConnected = data.deviceConnected; + detailsPage.connecting = data.connecting; + detailsPage.connected = data.connected; + detailsPage.showProxy = data.showProxy; + if (detailsPage.showProxy) + chrome.send('selectNetwork', [data.servicePath]); + + detailsPage.showStaticIPConfig = data.showStaticIPConfig; + $('connection-state').textContent = data.connectionState; + + var ipAutoConfig = data.ipAutoConfig ? 'automatic' : 'user'; + $('ip-automatic-configuration-checkbox').checked = data.ipAutoConfig; + var inetAddress = {autoConfig: ipAutoConfig}; + var inetNetmask = {autoConfig: ipAutoConfig}; + var inetGateway = {autoConfig: ipAutoConfig}; + + if (data.ipconfig.value) { + inetAddress.automatic = data.ipconfig.value.address; + inetAddress.value = data.ipconfig.value.address; + inetNetmask.automatic = data.ipconfig.value.netmask; + inetNetmask.value = data.ipconfig.value.netmask; + inetGateway.automatic = data.ipconfig.value.gateway; + inetGateway.value = data.ipconfig.value.gateway; + if (data.ipconfig.value.webProxyAutoDiscoveryUrl) { + $('web-proxy-auto-discovery').hidden = false; + $('web-proxy-auto-discovery-url').value = + data.ipconfig.value.webProxyAutoDiscoveryUrl; + } + } + + // Override the "automatic" values with the real saved DHCP values, + // if they are set. + if (data.savedIP.address) { + inetAddress.automatic = data.savedIP.address; + inetAddress.value = data.savedIP.address; + } + if (data.savedIP.netmask) { + inetNetmask.automatic = data.savedIP.netmask; + inetNetmask.value = data.savedIP.netmask; + } + if (data.savedIP.gateway) { + inetGateway.automatic = data.savedIP.gateway; + inetGateway.value = data.savedIP.gateway; + } + + if (ipAutoConfig == 'user') { + if (data.staticIP.value.address) { + inetAddress.value = data.staticIP.value.address; + inetAddress.user = data.staticIP.value.address; + } + if (data.staticIP.value.netmask) { + inetNetmask.value = data.staticIP.value.netmask; + inetNetmask.user = data.staticIP.value.netmask; + } + if (data.staticIP.value.gateway) { + inetGateway.value = data.staticIP.value.gateway; + inetGateway.user = data.staticIP.value.gateway; + } + } + + var configureAddressField = function(field, model) { + IPAddressField.decorate(field); + field.model = model; + field.editable = model.autoConfig == 'user'; + }; + + configureAddressField($('ip-address'), inetAddress); + configureAddressField($('ip-netmask'), inetNetmask); + configureAddressField($('ip-gateway'), inetGateway); + + var inetNameServers = ''; + if (data.ipconfig.value && data.ipconfig.value.nameServers) { + inetNameServers = data.ipconfig.value.nameServers; + $('automatic-dns-display').textContent = inetNameServers; + } + + if (data.savedIP && data.savedIP.nameServers) + $('automatic-dns-display').textContent = data.savedIP.nameServers; + + if (data.nameServersGoogle) + $('google-dns-display').textContent = data.nameServersGoogle; + + var nameServersUser = []; + if (data.staticIP.value.nameServers) + nameServersUser = data.staticIP.value.nameServers.split(','); + + var nameServerModels = []; + for (var i = 0; i < 4; ++i) + nameServerModels.push({value: nameServersUser[i] || ''}); + + $(data.nameServerType + '-dns-radio').checked = true; + configureAddressField($('ipconfig-dns1'), nameServerModels[0]); + configureAddressField($('ipconfig-dns2'), nameServerModels[1]); + configureAddressField($('ipconfig-dns3'), nameServerModels[2]); + configureAddressField($('ipconfig-dns4'), nameServerModels[3]); + + DetailsInternetPage.updateNameServerDisplay(data.nameServerType); + + if (data.hardwareAddress) { + $('hardware-address').textContent = data.hardwareAddress; + $('hardware-address-row').style.display = 'table-row'; + } else { + // This is most likely a device without a hardware address. + $('hardware-address-row').style.display = 'none'; + } + if (data.type == Constants.TYPE_WIFI) { + OptionsPage.showTab($('wifi-network-nav-tab')); + detailsPage.wireless = true; + detailsPage.vpn = false; + detailsPage.ethernet = false; + detailsPage.cellular = false; + detailsPage.gsm = false; + detailsPage.wimax = false; + detailsPage.shared = data.shared; + $('wifi-connection-state').textContent = data.connectionState; + $('wifi-ssid').textContent = data.ssid; + if (data.bssid && data.bssid.length > 0) { + $('wifi-bssid').textContent = data.bssid; + $('wifi-bssid-entry').hidden = false; + } else { + $('wifi-bssid-entry').hidden = true; + } + $('wifi-ip-address').textContent = inetAddress.value; + $('wifi-netmask').textContent = inetNetmask.value; + $('wifi-gateway').textContent = inetGateway.value; + $('wifi-name-servers').textContent = inetNameServers; + if (data.encryption && data.encryption.length > 0) { + $('wifi-security').textContent = data.encryption; + $('wifi-security-entry').hidden = false; + } else { + $('wifi-security-entry').hidden = true; + } + // Frequency is in MHz. + var frequency = loadTimeData.getString('inetFrequencyFormat'); + frequency = frequency.replace('$1', data.frequency); + $('wifi-frequency').textContent = frequency; + // Signal strength as percentage. + var signalStrength = loadTimeData.getString('inetSignalStrengthFormat'); + signalStrength = signalStrength.replace('$1', data.strength); + $('wifi-signal-strength').textContent = signalStrength; + if (data.hardwareAddress) { + $('wifi-hardware-address').textContent = data.hardwareAddress; + $('wifi-hardware-address-entry').hidden = false; + } else { + $('wifi-hardware-address-entry').hidden = true; + } + detailsPage.showPreferred = data.showPreferred; + $('prefer-network-wifi').checked = data.preferred.value; + $('prefer-network-wifi').disabled = !data.remembered; + $('auto-connect-network-wifi').checked = data.autoConnect.value; + $('auto-connect-network-wifi').disabled = !data.remembered; + detailsPage.password = data.encrypted; + } else if (data.type == Constants.TYPE_WIMAX) { + OptionsPage.showTab($('wimax-network-nav-tab')); + detailsPage.wimax = true; + detailsPage.wireless = false; + detailsPage.vpn = false; + detailsPage.ethernet = false; + detailsPage.cellular = false; + detailsPage.gsm = false; + detailsPage.shared = data.shared; + detailsPage.showPreferred = data.showPreferred; + $('wimax-connection-state').textContent = data.connectionState; + $('auto-connect-network-wimax').checked = data.autoConnect.value; + $('auto-connect-network-wimax').disabled = !data.remembered; + if (data.identity) { + $('wimax-eap-identity').textContent = data.identity; + $('wimax-eap-identity-entry').hidden = false; + } else { + $('wimax-eap-identity-entry').hidden = true; + } + // Signal strength as percentage. + var signalStrength = loadTimeData.getString('inetSignalStrengthFormat'); + signalStrength = signalStrength.replace('$1', data.strength); + $('wimax-signal-strength').textContent = signalStrength; + } else if (data.type == Constants.TYPE_CELLULAR) { + OptionsPage.showTab($('cellular-conn-nav-tab')); + detailsPage.ethernet = false; + detailsPage.wireless = false; + detailsPage.wimax = false; + detailsPage.vpn = false; + detailsPage.cellular = true; + if (data.showCarrierSelect && data.currentCarrierIndex != -1) { + var carrierSelector = $('select-carrier'); + carrierSelector.onchange = DetailsInternetPage.handleCarrierChanged; + carrierSelector.options.length = 0; + for (var i = 0; i < data.carriers.length; ++i) { + var option = document.createElement('option'); + option.textContent = data.carriers[i]; + carrierSelector.add(option); + } + carrierSelector.selectedIndex = data.currentCarrierIndex; + } else { + $('service-name').textContent = data.serviceName; + } + + $('network-technology').textContent = data.networkTechnology; + $('activation-state').textContent = data.activationState; + $('roaming-state').textContent = data.roamingState; + $('restricted-pool').textContent = data.restrictedPool; + $('error-state').textContent = data.errorState; + $('manufacturer').textContent = data.manufacturer; + $('model-id').textContent = data.modelId; + $('firmware-revision').textContent = data.firmwareRevision; + $('hardware-revision').textContent = data.hardwareRevision; + $('mdn').textContent = data.mdn; + $('operator-name').textContent = data.operatorName; + $('operator-code').textContent = data.operatorCode; + + // Make sure that GSM/CDMA specific properties that shouldn't be hidden + // are visible. + updateHidden('#details-internet-page .gsm-only', false); + updateHidden('#details-internet-page .cdma-only', false); + + // Show IMEI/ESN/MEID/MIN/PRL only if they are available. + (function() { + var setContentOrHide = function(property) { + var value = data[property]; + if (value) + $(property).textContent = value; + else + $(property).parentElement.hidden = true; + }; + setContentOrHide('esn'); + setContentOrHide('imei'); + setContentOrHide('meid'); + setContentOrHide('min'); + setContentOrHide('prl-version'); + })(); + detailsPage.gsm = data.gsm; + if (data.gsm) { + $('iccid').textContent = stringFromValue(data.iccid); + $('imsi').textContent = stringFromValue(data.imsi); + + var apnSelector = $('select-apn'); + // Clear APN lists, keep only last element that "other". + while (apnSelector.length != 1) + apnSelector.remove(0); + var otherOption = apnSelector[0]; + data.selectedApn = -1; + data.userApnIndex = -1; + var apnList = data.providerApnList.value; + for (var i = 0; i < apnList.length; i++) { + var option = document.createElement('option'); + var localizedName = apnList[i].localizedName; + var name = localizedName ? localizedName : apnList[i].name; + var apn = apnList[i].apn; + option.textContent = name ? (name + ' (' + apn + ')') : apn; + option.value = i; + // data.apn and data.lastGoodApn will always be defined, however + // data.apn.apn and data.lastGoodApn.apn may not be. This is not a + // problem, as apnList[i].apn will always be defined and the + // comparisons below will work as expected. + if ((data.apn.apn == apn && + data.apn.username == apnList[i].username && + data.apn.password == apnList[i].password) || + (!data.apn.apn && + data.lastGoodApn.apn == apn && + data.lastGoodApn.username == apnList[i].username && + data.lastGoodApn.password == apnList[i].password)) { + data.selectedApn = i; + } + // Insert new option before "other" option. + apnSelector.add(option, otherOption); + } + if (data.selectedApn == -1 && data.apn.apn) { + var option = document.createElement('option'); + option.textContent = data.apn.apn; + option.value = -1; + apnSelector.add(option, otherOption); + data.selectedApn = apnSelector.length - 2; + data.userApnIndex = data.selectedApn; + } + apnSelector.selectedIndex = data.selectedApn; + updateHidden('.apn-list-view', false); + updateHidden('.apn-details-view', true); + DetailsInternetPage.updateSecurityTab(data.simCardLockEnabled.value); + } + $('auto-connect-network-cellular').checked = data.autoConnect.value; + $('auto-connect-network-cellular').disabled = false; + + $('buyplan-details').hidden = !data.showBuyButton; + $('view-account-details').hidden = !data.showViewAccountButton; + $('activate-details').hidden = !data.showActivateButton; + if (data.showActivateButton) { + $('details-internet-login').hidden = true; + } + } else if (data.type == Constants.TYPE_VPN) { + OptionsPage.showTab($('vpn-nav-tab')); + detailsPage.wireless = false; + detailsPage.wimax = false; + detailsPage.vpn = true; + detailsPage.ethernet = false; + detailsPage.cellular = false; + detailsPage.gsm = false; + $('inet-service-name').textContent = data.serviceName; + $('inet-provider-type').textContent = data.providerType; + $('inet-username').textContent = data.username; + var inetServerHostname = $('inet-server-hostname'); + inetServerHostname.value = data.serverHostname.value; + inetServerHostname.resetHandler = function() { + OptionsPage.hideBubble(); + inetServerHostname.value = data.serverHostname.recommendedValue; + }; + $('auto-connect-network-vpn').checked = data.autoConnect.value; + $('auto-connect-network-vpn').disabled = false; + } else { + OptionsPage.showTab($('internet-nav-tab')); + detailsPage.ethernet = true; + detailsPage.wireless = false; + detailsPage.wimax = false; + detailsPage.vpn = false; + detailsPage.cellular = false; + detailsPage.gsm = false; + } + + // Update controlled option indicators. + indicators = cr.doc.querySelectorAll( + '#details-internet-page .controlled-setting-indicator'); + for (var i = 0; i < indicators.length; i++) { + var propName = indicators[i].getAttribute('data'); + if (!propName || !data[propName]) + continue; + var propData = data[propName]; + // Create a synthetic pref change event decorated as + // CoreOptionsHandler::CreateValueForPref() does. + var event = new Event(name); + event.value = { + value: propData.value, + controlledBy: propData.controlledBy, + recommendedValue: propData.recommendedValue, + }; + indicators[i].handlePrefChange(event); + var forElement = $(indicators[i].getAttribute('for')); + if (forElement) { + if (propData.controlledBy == 'policy') + forElement.disabled = true; + if (forElement.resetHandler) + indicators[i].resetHandler = forElement.resetHandler; + } + } + + detailsPage.updateControls(); + + // Don't show page name in address bar and in history to prevent people + // navigate here by hand and solve issue with page session restore. + OptionsPage.showPageByName('detailsInternetPage', false); + }; + + return { + DetailsInternetPage: DetailsInternetPage + }; +}); diff --git a/chromium/chrome/browser/resources/options/chromeos/internet_detail_ip_address_field.js b/chromium/chrome/browser/resources/options/chromeos/internet_detail_ip_address_field.js new file mode 100644 index 00000000000..d0a096c77d0 --- /dev/null +++ b/chromium/chrome/browser/resources/options/chromeos/internet_detail_ip_address_field.js @@ -0,0 +1,111 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options.internet', function() { + /** @const */ var EditableTextField = options.EditableTextField; + + /** + * The regular expression that matches an IP address. String to match against + * should have all whitespace stripped already. + * @const + * @type {RegExp} + */ + var singleIp_ = /^([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)$/; + + /** + * Creates a new field specifically for entering IP addresses. + * @constructor + */ + function IPAddressField() { + var el = cr.doc.createElement('div'); + IPAddressField.decorate(el); + return el; + } + + /** + * Decorates an element as a inline-editable list item. Note that this is + * a subclass of IPAddressField. + * @param {!HTMLElement} el The element to decorate. + */ + IPAddressField.decorate = function(el) { + el.__proto__ = IPAddressField.prototype; + el.decorate(); + }; + + IPAddressField.prototype = { + __proto__: EditableTextField.prototype, + + /** @override */ + decorate: function() { + EditableTextField.prototype.decorate.call(this); + }, + + /** + * Indicates whether or not empty values are allowed. + * @type {boolean} + */ + get allowEmpty() { + return this.hasAttribute('allow-empty'); + }, + + /** @override */ + get currentInputIsValid() { + if (!this.editField.value && this.allowEmpty) + return true; + + // Make sure it's only got numbers and ".", there are the correct + // count of them, and they are all within the correct range. + var fieldValue = this.editField.value.replace(/\s/g, ''); + var matches = singleIp_.exec(fieldValue); + var rangeCorrect = true; + if (matches != null) { + for (var i = 1; i < matches.length; ++i) { + var value = parseInt(matches[i], 10); + if (value < 0 || value > 255) { + rangeCorrect = false; + break; + } + } + } + return this.editField.validity.valid && matches != null && + rangeCorrect && matches.length == 5; + }, + + /** @override */ + get hasBeenEdited() { + return this.editField.value != this.model.value; + }, + + /** + * Overrides superclass to mutate the input during a successful commit. For + * the purposes of entering IP addresses, this just means stripping off + * whitespace and leading zeros from each of the octets so that they conform + * to the normal format for IP addresses. + * @override + * @param {string} value Input IP address to be mutated. + * @return {string} mutated IP address. + */ + mutateInput: function(value) { + if (!value) + return value; + + var fieldValue = value.replace(/\s/g, ''); + var matches = singleIp_.exec(fieldValue); + var result = []; + + // If we got this far, matches shouldn't be null, but make sure. + if (matches != null) { + // starting at one because the first match element contains the entire + // match, and we don't care about that. + for (var i = 1; i < matches.length; ++i) + result.push(parseInt(matches[i], 10)); + } + return result.join('.'); + }, + }; + + return { + IPAddressField: IPAddressField, + }; +}); diff --git a/chromium/chrome/browser/resources/options/chromeos/keyboard_overlay.css b/chromium/chrome/browser/resources/options/chromeos/keyboard_overlay.css new file mode 100644 index 00000000000..316e07af902 --- /dev/null +++ b/chromium/chrome/browser/resources/options/chromeos/keyboard_overlay.css @@ -0,0 +1,7 @@ +/* Copyright 2013 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ + +#send-function-keys-description { + color: gray; +} diff --git a/chromium/chrome/browser/resources/options/chromeos/keyboard_overlay.html b/chromium/chrome/browser/resources/options/chromeos/keyboard_overlay.html new file mode 100644 index 00000000000..039c52e7954 --- /dev/null +++ b/chromium/chrome/browser/resources/options/chromeos/keyboard_overlay.html @@ -0,0 +1,100 @@ +<div id="keyboard-overlay" class="page" hidden> + <div class="close-button"></div> + <h1 i18n-content="keyboardOverlayTitle"></h1> + <div class="content-area"> + <table class="option-control-table"> + <tr> + <td> + <label class="option-name" for="remap-search-key-to" + i18n-content="remapSearchKeyToContent"> + </label> + </td> + <td class="option-value"> + <select id="remap-search-key-to" class="control" + data-type="number" i18n-options="remapSearchKeyToValue" + pref="settings.language.xkb_remap_search_key_to" dialog-pref> + </select> + </td> + </tr> + <tr> + <td> + <label class="option-name" for="remap-control-key-to" + i18n-content="remapControlKeyToContent"> + </label> + </td> + <td class="option-value"> + <select id="remap-control-key-to" class="control" + data-type="number" i18n-options="remapControlKeyToValue" + pref="settings.language.xkb_remap_control_key_to" dialog-pref> + </select> + </td> + </tr> + <tr> + <td> + <label class="option-name" for="remap-alt-key-to" + i18n-content="remapAltKeyToContent"> + </label> + </td> + <td class="option-value"> + <select id="remap-alt-key-to" class="control" data-type="number" + pref="settings.language.xkb_remap_alt_key_to" + i18n-options="remapAltKeyToValue" dialog-pref></select> + </td> + </tr> + <!-- The caps lock section is hidden by default. This is only visible + when --has-chromeos-keyboard flag is not passed. --> + <tr id="caps-lock-remapping-section" hidden> + <td> + <label class="option-name" for="remap-caps-lock-key-to" + i18n-content="remapCapsLockKeyToContent"> + </label> + </td> + <td class="option-value"> + <select id="remap-caps-lock-key-to" class="control" + data-type="number" + pref="settings.language.remap_caps_lock_key_to" + i18n-options="remapCapsLockKeyToValue" dialog-pref></select> + </td> + </tr> + <!-- The diamond key section is hidden by default. This is only visible + when --has-chromeos-diamond-key flag is passed. --> + <tr id="diamond-key-remapping-section" hidden> + <td> + <label class="option-name" for="remap-diamond-key-to" + i18n-content="remapDiamondKeyToContent"> + </label> + </td> + <td class="option-value"> + <select id="remap-diamond-key-to" class="control" + data-type="number" + pref="settings.language.remap_diamond_key_to" + i18n-options="remapDiamondKeyToValue" dialog-pref></select> + </td> + </tr> + </table> + <div class="settings-row"> + <div class="checkbox"> + <label> + <input id="send-function-keys" type="checkbox" + pref="settings.language.send_function_keys" dialog-pref> + <span i18n-content="sendFunctionKeys"></span> + </label> + </div> + <label id="send-function-keys-description" for="send-function-keys" + i18n-content="sendFunctionKeysDescription"> + </label> + </div> + </div> + <div class="content-area"> + <button id="languages-and-input-settings" class="link-button" + i18n-content="changeLanguageAndInputSettings"></button> + </div> + <div class="action-area"> + <div class="button-strip"> + <button id="keyboard-cancel" type="reset" i18n-content="cancel"></button> + <button id="keyboard-confirm" class="default-button" type="submit" + i18n-content="ok"> + </button> + </div> + </div> +</div> diff --git a/chromium/chrome/browser/resources/options/chromeos/keyboard_overlay.js b/chromium/chrome/browser/resources/options/chromeos/keyboard_overlay.js new file mode 100644 index 00000000000..39c8a694fb3 --- /dev/null +++ b/chromium/chrome/browser/resources/options/chromeos/keyboard_overlay.js @@ -0,0 +1,66 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + + /** + * Encapsulated handling of the keyboard overlay. + * @constructor + */ + function KeyboardOverlay() { + options.SettingsDialog.call(this, 'keyboard-overlay', + loadTimeData.getString('keyboardOverlayTitle'), + 'keyboard-overlay', + $('keyboard-confirm'), $('keyboard-cancel')); + } + + cr.addSingletonGetter(KeyboardOverlay); + + KeyboardOverlay.prototype = { + __proto__: options.SettingsDialog.prototype, + + /** + * Initializes the page. This method is called in initialize. + */ + initializePage: function() { + options.SettingsDialog.prototype.initializePage.call(this); + + $('languages-and-input-settings').onclick = function(e) { + OptionsPage.navigateToPage('languages'); + }; + }, + + /** + * Show/hide the caps lock remapping section. + * @private + */ + showCapsLockOptions_: function(show) { + $('caps-lock-remapping-section').hidden = !show; + }, + + /** + * Show/hide the diamond key remapping section. + * @private + */ + showDiamondKeyOptions_: function(show) { + $('diamond-key-remapping-section').hidden = !show; + }, + }; + + // Forward public APIs to private implementations. + [ + 'showCapsLockOptions', + 'showDiamondKeyOptions', + ].forEach(function(name) { + KeyboardOverlay[name] = function() { + var instance = KeyboardOverlay.getInstance(); + return instance[name + '_'].apply(instance, arguments); + }; + }); + + // Export + return { + KeyboardOverlay: KeyboardOverlay + }; +}); diff --git a/chromium/chrome/browser/resources/options/chromeos/network_list.js b/chromium/chrome/browser/resources/options/chromeos/network_list.js new file mode 100644 index 00000000000..4d20937fa5f --- /dev/null +++ b/chromium/chrome/browser/resources/options/chromeos/network_list.js @@ -0,0 +1,1174 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options.network', function() { + + var ArrayDataModel = cr.ui.ArrayDataModel; + var List = cr.ui.List; + var ListItem = cr.ui.ListItem; + var ListSingleSelectionModel = cr.ui.ListSingleSelectionModel; + var Menu = cr.ui.Menu; + var MenuItem = cr.ui.MenuItem; + var ControlledSettingIndicator = options.ControlledSettingIndicator; + + /** + * Network settings constants. These enums usually match their C++ + * counterparts. + */ + function Constants() {} + + // Network types: + Constants.TYPE_UNKNOWN = 'UNKNOWN'; + Constants.TYPE_ETHERNET = 'ethernet'; + Constants.TYPE_WIFI = 'wifi'; + Constants.TYPE_WIMAX = 'wimax'; + Constants.TYPE_BLUETOOTH = 'bluetooth'; + Constants.TYPE_CELLULAR = 'cellular'; + Constants.TYPE_VPN = 'vpn'; + + // Cellular activation states: + Constants.ACTIVATION_STATE_UNKNOWN = 0; + Constants.ACTIVATION_STATE_ACTIVATED = 1; + Constants.ACTIVATION_STATE_ACTIVATING = 2; + Constants.ACTIVATION_STATE_NOT_ACTIVATED = 3; + Constants.ACTIVATION_STATE_PARTIALLY_ACTIVATED = 4; + + /** + * Order in which controls are to appear in the network list sorted by key. + */ + Constants.NETWORK_ORDER = ['ethernet', + 'wifi', + 'wimax', + 'cellular', + 'vpn', + 'addConnection']; + + /** + * Mapping of network category titles to the network type. + */ + var categoryMap = { + 'cellular': Constants.TYPE_CELLULAR, + 'ethernet': Constants.TYPE_ETHERNET, + 'wimax': Constants.TYPE_WIMAX, + 'wifi': Constants.TYPE_WIFI, + 'vpn': Constants.TYPE_VPN + }; + + /** + * ID of the menu that is currently visible. + * @type {?string} + * @private + */ + var activeMenu_ = null; + + /** + * Indicates if cellular networks are available. + * @type {boolean} + * @private + */ + var cellularAvailable_ = false; + + /** + * Indicates if cellular networks are enabled. + * @type {boolean} + * @private + */ + var cellularEnabled_ = false; + + /** + * Indicates if cellular device supports network scanning. + * @type {boolean} + * @private + */ + var cellularSupportsScan_ = false; + + /** + * Indicates if WiMAX networks are available. + * @type {boolean} + * @private + */ + var wimaxAvailable_ = false; + + /** + * Indicates if WiMAX networks are enabled. + * @type {boolean} + * @private + */ + var wimaxEnabled_ = false; + + /** + * Indicates if mobile data roaming is enabled. + * @type {boolean} + * @private + */ + var enableDataRoaming_ = false; + + /** + * Icon to use when not connected to a particular type of network. + * @type {!Object.<string, string>} Mapping of network type to icon data url. + * @private + */ + var defaultIcons_ = {}; + + /** + * Contains the current logged in user type, which is one of 'none', + * 'regular', 'owner', 'guest', 'retail-mode', 'public-account', + * 'locally-managed', and 'kiosk-app', or empty string if the data has not + * been set. + * @type {string} + * @private + */ + var loggedInUserType_ = ''; + + /** + * Create an element in the network list for controlling network + * connectivity. + * @param {Object} data Description of the network list or command. + * @constructor + */ + function NetworkListItem(data) { + var el = cr.doc.createElement('li'); + el.data_ = {}; + for (var key in data) + el.data_[key] = data[key]; + NetworkListItem.decorate(el); + return el; + } + + /** + * Decorate an element as a NetworkListItem. + * @param {!Element} el The element to decorate. + */ + NetworkListItem.decorate = function(el) { + el.__proto__ = NetworkListItem.prototype; + el.decorate(); + }; + + NetworkListItem.prototype = { + __proto__: ListItem.prototype, + + /** + * Description of the network group or control. + * @type {Object.<string,Object>} + * @private + */ + data_: null, + + /** + * Element for the control's subtitle. + * @type {?Element} + * @private + */ + subtitle_: null, + + /** + * Icon for the network control. + * @type {?Element} + * @private + */ + icon_: null, + + /** + * Indicates if in the process of connecting to a network. + * @type {boolean} + * @private + */ + connecting_: false, + + /** + * Description of the network control. + * @type {Object} + */ + get data() { + return this.data_; + }, + + /** + * Text label for the subtitle. + * @type {string} + */ + set subtitle(text) { + if (text) + this.subtitle_.textContent = text; + this.subtitle_.hidden = !text; + }, + + /** + * URL for the network icon. + * @type {string} + */ + set iconURL(iconURL) { + this.icon_.style.backgroundImage = url(iconURL); + }, + + /** + * Type of network icon. Each type corresponds to a CSS rule. + * @type {string} + */ + set iconType(type) { + if (defaultIcons_[type]) + this.iconURL = defaultIcons_[type]; + else + this.icon_.classList.add('network-' + type); + }, + + /** + * Indicates if the network is in the process of being connected. + * @type {boolean} + */ + set connecting(state) { + this.connecting_ = state; + if (state) + this.icon_.classList.add('network-connecting'); + else + this.icon_.classList.remove('network-connecting'); + }, + + /** + * Indicates if the network is in the process of being connected. + * @type {boolean} + */ + get connecting() { + return this.connecting_; + }, + + /** + * Set the direction of the text. + * @param {string} direction The direction of the text, e.g. 'ltr'. + */ + setSubtitleDirection: function(direction) { + this.subtitle_.dir = direction; + }, + + /** + * Indicate that the selector arrow should be shown. + */ + showSelector: function() { + this.subtitle_.classList.add('network-selector'); + }, + + /** + * Adds an indicator to show that the network is policy managed. + */ + showManagedNetworkIndicator: function() { + this.appendChild(new ManagedNetworkIndicator()); + }, + + /** @override */ + decorate: function() { + ListItem.prototype.decorate.call(this); + this.className = 'network-group'; + this.icon_ = this.ownerDocument.createElement('div'); + this.icon_.className = 'network-icon'; + this.appendChild(this.icon_); + var textContent = this.ownerDocument.createElement('div'); + textContent.className = 'network-group-labels'; + this.appendChild(textContent); + var categoryLabel = this.ownerDocument.createElement('div'); + var title = this.data_.key + 'Title'; + categoryLabel.className = 'network-title'; + categoryLabel.textContent = loadTimeData.getString(title); + textContent.appendChild(categoryLabel); + this.subtitle_ = this.ownerDocument.createElement('div'); + this.subtitle_.className = 'network-subtitle'; + textContent.appendChild(this.subtitle_); + }, + }; + + /** + * Creates a control that displays a popup menu when clicked. + * @param {Object} data Description of the control. + */ + function NetworkMenuItem(data) { + var el = new NetworkListItem(data); + el.__proto__ = NetworkMenuItem.prototype; + el.decorate(); + return el; + } + + NetworkMenuItem.prototype = { + __proto__: NetworkListItem.prototype, + + /** + * Popup menu element. + * @type {?Element} + * @private + */ + menu_: null, + + /** @override */ + decorate: function() { + this.subtitle = null; + if (this.data.iconType) + this.iconType = this.data.iconType; + this.addEventListener('click', function() { + this.showMenu(); + }); + }, + + /** + * Retrieves the ID for the menu. + */ + getMenuName: function() { + return this.data_.key + '-network-menu'; + }, + + /** + * Creates a popup menu for the control. + * @return {Element} The newly created menu. + */ + createMenu: function() { + if (this.data.menu) { + var menu = this.ownerDocument.createElement('div'); + menu.id = this.getMenuName(); + menu.className = 'network-menu'; + menu.hidden = true; + Menu.decorate(menu); + for (var i = 0; i < this.data.menu.length; i++) { + var entry = this.data.menu[i]; + createCallback_(menu, null, entry.label, entry.command); + } + return menu; + } + return null; + }, + + canUpdateMenu: function() { + return false; + }, + + /** + * Displays a popup menu. + */ + showMenu: function() { + var rebuild = false; + // Force a rescan if opening the menu for WiFi networks to ensure the + // list is up to date. Networks are periodically rescanned, but depending + // on timing, there could be an excessive delay before the first rescan + // unless forced. + var rescan = !activeMenu_ && this.data_.key == 'wifi'; + if (!this.menu_) { + rebuild = true; + var existing = $(this.getMenuName()); + if (existing) { + if (this.updateMenu()) + return; + closeMenu_(); + } + this.menu_ = this.createMenu(); + this.menu_.addEventListener('mousedown', function(e) { + // Prevent blurring of list, which would close the menu. + e.preventDefault(); + }); + var parent = $('network-menus'); + if (existing) + parent.replaceChild(this.menu_, existing); + else + parent.appendChild(this.menu_); + } + var top = this.offsetTop + this.clientHeight; + var menuId = this.getMenuName(); + if (menuId != activeMenu_ || rebuild) { + closeMenu_(); + activeMenu_ = menuId; + this.menu_.style.setProperty('top', top + 'px'); + this.menu_.hidden = false; + } + if (rescan) + chrome.send('refreshNetworks'); + }, + }; + + /** + * Creates a control for selecting or configuring a network connection based + * on the type of connection (e.g. wifi versus vpn). + * @param {{key: string, + * networkList: Array.<Object>} data Description of the network. + * @constructor + */ + function NetworkSelectorItem(data) { + var el = new NetworkMenuItem(data); + el.__proto__ = NetworkSelectorItem.prototype; + el.decorate(); + return el; + } + + NetworkSelectorItem.prototype = { + __proto__: NetworkMenuItem.prototype, + + /** @override */ + decorate: function() { + // TODO(kevers): Generalize method of setting default label. + var policyManaged = false; + var defaultMessage = this.data_.key == 'wifi' ? + 'networkOffline' : 'networkNotConnected'; + this.subtitle = loadTimeData.getString(defaultMessage); + var list = this.data_.networkList; + var candidateURL = null; + for (var i = 0; i < list.length; i++) { + var networkDetails = list[i]; + if (networkDetails.connecting || networkDetails.connected) { + this.subtitle = networkDetails.networkName; + this.setSubtitleDirection('ltr'); + policyManaged = networkDetails.policyManaged; + candidateURL = networkDetails.iconURL; + // Only break when we see a connecting network as it is possible to + // have a connected network and a connecting network at the same + // time. + if (networkDetails.connecting) { + this.connecting = true; + candidateURL = null; + break; + } + } + } + if (candidateURL) + this.iconURL = candidateURL; + else + this.iconType = this.data.key; + + this.showSelector(); + + if (policyManaged) + this.showManagedNetworkIndicator(); + + if (activeMenu_ == this.getMenuName()) { + // Menu is already showing and needs to be updated. Explicitly calling + // show menu will force the existing menu to be replaced. The call + // is deferred in order to ensure that position of this element has + // beem properly updated. + var self = this; + setTimeout(function() {self.showMenu();}, 0); + } + }, + + /** + * Creates a menu for selecting, configuring or disconnecting from a + * network. + * @return {Element} The newly created menu. + */ + createMenu: function() { + var menu = this.ownerDocument.createElement('div'); + menu.id = this.getMenuName(); + menu.className = 'network-menu'; + menu.hidden = true; + Menu.decorate(menu); + var addendum = []; + if (this.data_.key == 'wifi') { + addendum.push({label: loadTimeData.getString('joinOtherNetwork'), + command: 'add', + data: {networkType: Constants.TYPE_WIFI, + servicePath: ''}}); + } else if (this.data_.key == 'cellular') { + if (cellularEnabled_ && cellularSupportsScan_) { + entry = { + label: loadTimeData.getString('otherCellularNetworks'), + command: createAddConnectionCallback_(Constants.TYPE_CELLULAR), + addClass: ['other-cellulars'], + data: {} + }; + addendum.push(entry); + } + + var label = enableDataRoaming_ ? 'disableDataRoaming' : + 'enableDataRoaming'; + var disabled = loggedInUserType_ != 'owner'; + var entry = {label: loadTimeData.getString(label), + data: {}}; + if (disabled) { + entry.command = null; + entry.tooltip = + loadTimeData.getString('dataRoamingDisableToggleTooltip'); + } else { + var self = this; + entry.command = function() { + options.Preferences.setBooleanPref( + 'cros.signed.data_roaming_enabled', + !enableDataRoaming_, true); + // Force revalidation of the menu the next time it is displayed. + self.menu_ = null; + }; + } + addendum.push(entry); + } + var list = this.data.rememberedNetworks; + if (list && list.length > 0) { + var callback = function(list) { + $('remembered-network-list').clear(); + var dialog = options.PreferredNetworks.getInstance(); + OptionsPage.showPageByName('preferredNetworksPage', false); + dialog.update(list); + }; + addendum.push({label: loadTimeData.getString('preferredNetworks'), + command: callback, + data: list}); + } + + var networkGroup = this.ownerDocument.createElement('div'); + networkGroup.className = 'network-menu-group'; + list = this.data.networkList; + var empty = !list || list.length == 0; + if (list) { + for (var i = 0; i < list.length; i++) { + var data = list[i]; + this.createNetworkOptionsCallback_(networkGroup, data); + if (data.connected) { + if (data.networkType == Constants.TYPE_VPN) { + // Add separator + addendum.push({}); + var i18nKey = 'disconnectNetwork'; + addendum.push({label: loadTimeData.getString(i18nKey), + command: 'disconnect', + data: data}); + } + } + } + } + if (this.data_.key == 'wifi' || this.data_.key == 'wimax' || + this.data_.key == 'cellular') { + addendum.push({}); + if (this.data_.key == 'wifi') { + addendum.push({label: loadTimeData.getString('turnOffWifi'), + command: function() { + chrome.send('disableWifi'); + }, + data: {}}); + } else if (this.data_.key == 'wimax') { + addendum.push({label: loadTimeData.getString('turnOffWimax'), + command: function() { + chrome.send('disableWimax'); + }, + data: {}}); + } else if (this.data_.key == 'cellular') { + addendum.push({label: loadTimeData.getString('turnOffCellular'), + command: function() { + chrome.send('disableCellular'); + }, + data: {}}); + } + } + if (!empty) + menu.appendChild(networkGroup); + if (addendum.length > 0) { + var separator = false; + if (!empty) { + menu.appendChild(MenuItem.createSeparator()); + separator = true; + } + for (var i = 0; i < addendum.length; i++) { + var value = addendum[i]; + if (value.data) { + var item = createCallback_(menu, value.data, value.label, + value.command); + if (value.tooltip) + item.title = value.tooltip; + if (value.addClass) + item.classList.add(value.addClass); + separator = false; + } else if (!separator) { + menu.appendChild(MenuItem.createSeparator()); + separator = true; + } + } + } + return menu; + }, + + /** + * Determines if a menu can be updated on the fly. Menus that cannot be + * updated are fully regenerated using createMenu. The advantage of + * updating a menu is that it can preserve ordering of networks avoiding + * entries from jumping around after an update. + */ + canUpdateMenu: function() { + return this.data_.key == 'wifi' && activeMenu_ == this.getMenuName(); + }, + + /** + * Updates an existing menu. Updated menus preserve ordering of prior + * entries. During the update process, the ordering may differ from the + * preferred ordering as determined by the network library. If the + * ordering becomes potentially out of sync, then the updated menu is + * marked for disposal on close. Reopening the menu will force a + * regeneration, which will in turn fix the ordering. + * @return {boolean} True if successfully updated. + */ + updateMenu: function() { + if (!this.canUpdateMenu()) + return false; + var oldMenu = $(this.getMenuName()); + var group = oldMenu.getElementsByClassName('network-menu-group')[0]; + if (!group) + return false; + var newMenu = this.createMenu(); + var discardOnClose = false; + var oldNetworkButtons = this.extractNetworkConnectButtons_(oldMenu); + var newNetworkButtons = this.extractNetworkConnectButtons_(newMenu); + for (var key in oldNetworkButtons) { + if (newNetworkButtons[key]) { + group.replaceChild(newNetworkButtons[key].button, + oldNetworkButtons[key].button); + if (newNetworkButtons[key].index != oldNetworkButtons[key].index) + discardOnClose = true; + newNetworkButtons[key] = null; + } else { + // Leave item in list to prevent network items from jumping due to + // deletions. + oldNetworkButtons[key].disabled = true; + discardOnClose = true; + } + } + for (var key in newNetworkButtons) { + var entry = newNetworkButtons[key]; + if (entry) { + group.appendChild(entry.button); + discardOnClose = true; + } + } + oldMenu.data = {discardOnClose: discardOnClose}; + return true; + }, + + /** + * Extracts a mapping of network names to menu element and position. + * @param {!Element} menu The menu to process. + * @return {Object.<string, Element>} Network mapping. + * @private + */ + extractNetworkConnectButtons_: function(menu) { + var group = menu.getElementsByClassName('network-menu-group')[0]; + var networkButtons = {}; + if (!group) + return networkButtons; + var buttons = group.getElementsByClassName('network-menu-item'); + for (var i = 0; i < buttons.length; i++) { + var label = buttons[i].data.label; + networkButtons[label] = {index: i, button: buttons[i]}; + } + return networkButtons; + }, + + /** + * Adds a menu item for showing network details. + * @param {!Element} parent The parent element. + * @param {Object} data Description of the network. + * @private + */ + createNetworkOptionsCallback_: function(parent, data) { + var menuItem = createCallback_(parent, + data, + data.networkName, + 'options', + data.iconURL); + if (data.policyManaged) + menuItem.appendChild(new ManagedNetworkIndicator()); + if (data.connected || data.connecting) { + var label = menuItem.getElementsByClassName( + 'network-menu-item-label')[0]; + label.classList.add('active-network'); + } + } + }; + + /** + * Creates a button-like control for configurating internet connectivity. + * @param {{key: string, + * subtitle: string, + * command: function} data Description of the network control. + * @constructor + */ + function NetworkButtonItem(data) { + var el = new NetworkListItem(data); + el.__proto__ = NetworkButtonItem.prototype; + el.decorate(); + return el; + } + + NetworkButtonItem.prototype = { + __proto__: NetworkListItem.prototype, + + /** @override */ + decorate: function() { + if (this.data.subtitle) + this.subtitle = this.data.subtitle; + else + this.subtitle = null; + if (this.data.command) + this.addEventListener('click', this.data.command); + if (this.data.iconURL) + this.iconURL = this.data.iconURL; + else if (this.data.iconType) + this.iconType = this.data.iconType; + if (this.data.policyManaged) + this.showManagedNetworkIndicator(); + }, + }; + + /** + * Adds a command to a menu for modifying network settings. + * @param {!Element} menu Parent menu. + * @param {!Object} data Description of the network. + * @param {!string} label Display name for the menu item. + * @param {?(string|function)} command Callback function or name + * of the command for |networkCommand|. + * @param {?string=} opt_iconURL Optional URL to an icon for the menu item. + * @return {!Element} The created menu item. + * @private + */ + function createCallback_(menu, data, label, command, opt_iconURL) { + var button = menu.ownerDocument.createElement('div'); + button.className = 'network-menu-item'; + + var buttonIcon = menu.ownerDocument.createElement('div'); + buttonIcon.className = 'network-menu-item-icon'; + button.appendChild(buttonIcon); + if (opt_iconURL) + buttonIcon.style.backgroundImage = url(opt_iconURL); + + var buttonLabel = menu.ownerDocument.createElement('span'); + buttonLabel.className = 'network-menu-item-label'; + buttonLabel.textContent = label; + button.appendChild(buttonLabel); + var callback = null; + if (typeof command == 'string') { + var type = data.networkType; + var path = data.servicePath; + callback = function() { + chrome.send('networkCommand', + [type, path, command]); + closeMenu_(); + }; + } else if (command != null) { + if (data) { + callback = function() { + command(data); + closeMenu_(); + }; + } else { + callback = function() { + command(); + closeMenu_(); + }; + } + } + if (callback != null) + button.addEventListener('click', callback); + else + buttonLabel.classList.add('network-disabled-control'); + + button.data = {label: label}; + MenuItem.decorate(button); + menu.appendChild(button); + return button; + } + + /** + * A list of controls for manipulating network connectivity. + * @constructor + */ + var NetworkList = cr.ui.define('list'); + + NetworkList.prototype = { + __proto__: List.prototype, + + /** @override */ + decorate: function() { + List.prototype.decorate.call(this); + this.startBatchUpdates(); + this.autoExpands = true; + this.dataModel = new ArrayDataModel([]); + this.selectionModel = new ListSingleSelectionModel(); + this.addEventListener('blur', this.onBlur_.bind(this)); + this.selectionModel.addEventListener('change', + this.onSelectionChange_.bind(this)); + + // Wi-Fi control is always visible. + this.update({key: 'wifi', networkList: []}); + + var entryAddWifi = { + label: loadTimeData.getString('addConnectionWifi'), + command: createAddConnectionCallback_(Constants.TYPE_WIFI) + }; + var entryAddVPN = { + label: loadTimeData.getString('addConnectionVPN'), + command: createAddConnectionCallback_(Constants.TYPE_VPN) + }; + this.update({key: 'addConnection', + iconType: 'add-connection', + menu: [entryAddWifi, entryAddVPN] + }); + + var prefs = options.Preferences.getInstance(); + prefs.addEventListener('cros.signed.data_roaming_enabled', + function(event) { + enableDataRoaming_ = event.value.value; + }); + this.endBatchUpdates(); + }, + + /** + * When the list loses focus, unselect all items in the list and close the + * active menu. + * @private + */ + onBlur_: function() { + this.selectionModel.unselectAll(); + closeMenu_(); + }, + + /** + * Close bubble and menu when a different list item is selected. + * @param {Event} event Event detailing the selection change. + * @private + */ + onSelectionChange_: function(event) { + OptionsPage.hideBubble(); + // A list item may temporarily become unselected while it is constructing + // its menu. The menu should therefore only be closed if a different item + // is selected, not when the menu's owner item is deselected. + if (activeMenu_) { + for (var i = 0; i < event.changes.length; ++i) { + if (event.changes[i].selected) { + var item = this.dataModel.item(event.changes[i].index); + if (!item.getMenuName || item.getMenuName() != activeMenu_) { + closeMenu_(); + return; + } + } + } + } + }, + + /** + * Finds the index of a network item within the data model based on + * category. + * @param {string} key Unique key for the item in the list. + * @return {number} The index of the network item, or |undefined| if it is + * not found. + */ + indexOf: function(key) { + var size = this.dataModel.length; + for (var i = 0; i < size; i++) { + var entry = this.dataModel.item(i); + if (entry.key == key) + return i; + } + }, + + /** + * Updates a network control. + * @param {Object.<string,string>} data Description of the entry. + */ + update: function(data) { + this.startBatchUpdates(); + var index = this.indexOf(data.key); + if (index == undefined) { + // Find reference position for adding the element. We cannot hide + // individual list elements, thus we need to conditionally add or + // remove elements and cannot rely on any element having a fixed index. + for (var i = 0; i < Constants.NETWORK_ORDER.length; i++) { + if (data.key == Constants.NETWORK_ORDER[i]) { + data.sortIndex = i; + break; + } + } + var referenceIndex = -1; + for (var i = 0; i < this.dataModel.length; i++) { + var entry = this.dataModel.item(i); + if (entry.sortIndex < data.sortIndex) + referenceIndex = i; + else + break; + } + if (referenceIndex == -1) { + // Prepend to the start of the list. + this.dataModel.splice(0, 0, data); + } else if (referenceIndex == this.dataModel.length) { + // Append to the end of the list. + this.dataModel.push(data); + } else { + // Insert after the reference element. + this.dataModel.splice(referenceIndex + 1, 0, data); + } + } else { + var entry = this.dataModel.item(index); + data.sortIndex = entry.sortIndex; + this.dataModel.splice(index, 1, data); + } + this.endBatchUpdates(); + }, + + /** @override */ + createItem: function(entry) { + if (entry.networkList) + return new NetworkSelectorItem(entry); + if (entry.command) + return new NetworkButtonItem(entry); + if (entry.menu) + return new NetworkMenuItem(entry); + }, + + /** + * Deletes an element from the list. + * @param {string} key Unique identifier for the element. + */ + deleteItem: function(key) { + var index = this.indexOf(key); + if (index != undefined) + this.dataModel.splice(index, 1); + }, + + /** + * Updates the state of a toggle button. + * @param {string} key Unique identifier for the element. + * @param {boolean} active Whether the control is active. + */ + updateToggleControl: function(key, active) { + var index = this.indexOf(key); + if (index != undefined) { + var entry = this.dataModel.item(index); + entry.iconType = active ? 'control-active' : + 'control-inactive'; + this.update(entry); + } + } + }; + + /** + * Sets the default icon to use for each network type if disconnected. + * @param {!Object.<string, string>} data Mapping of network type to icon + * data url. + */ + NetworkList.setDefaultNetworkIcons = function(data) { + defaultIcons_ = Object.create(data); + }; + + /** + * Sets the current logged in user type. + * @param {string} userType Current logged in user type. + */ + NetworkList.updateLoggedInUserType = function(userType) { + loggedInUserType_ = String(userType); + }; + + /** + * Chrome callback for updating network controls. + * @param {Object} data Description of available network devices and their + * corresponding state. + */ + NetworkList.refreshNetworkData = function(data) { + var networkList = $('network-list'); + networkList.startBatchUpdates(); + cellularAvailable_ = data.cellularAvailable; + cellularEnabled_ = data.cellularEnabled; + cellularSupportsScan_ = data.cellularSupportsScan; + wimaxAvailable_ = data.wimaxAvailable; + wimaxEnabled_ = data.wimaxEnabled; + + // Only show Ethernet control if connected. + var ethernetConnection = getConnection_(data.wiredList); + if (ethernetConnection) { + var type = String(Constants.TYPE_ETHERNET); + var path = ethernetConnection.servicePath; + var ethernetOptions = function() { + chrome.send('networkCommand', + [type, path, 'options']); + }; + networkList.update({key: 'ethernet', + subtitle: loadTimeData.getString('networkConnected'), + iconURL: ethernetConnection.iconURL, + command: ethernetOptions, + policyManaged: ethernetConnection.policyManaged}); + } else { + networkList.deleteItem('ethernet'); + } + + if (data.wifiEnabled) + loadData_('wifi', data.wirelessList, data.rememberedList); + else + addEnableNetworkButton_('wifi', 'enableWifi', 'wifi'); + + // Only show cellular control if available. + if (data.cellularAvailable) { + if (data.cellularEnabled) + loadData_('cellular', data.wirelessList, data.rememberedList); + else + addEnableNetworkButton_('cellular', 'enableCellular', 'cellular'); + } else { + networkList.deleteItem('cellular'); + } + + // Only show cellular control if available. + if (data.wimaxAvailable) { + if (data.wimaxEnabled) + loadData_('wimax', data.wirelessList, data.rememberedList); + else + addEnableNetworkButton_('wimax', 'enableWimax', 'cellular'); + } else { + networkList.deleteItem('wimax'); + } + + // Only show VPN control if there is at least one VPN configured. + if (data.vpnList.length > 0) + loadData_('vpn', data.vpnList, data.rememberedList); + else + networkList.deleteItem('vpn'); + networkList.endBatchUpdates(); + }; + + /** + * Replaces a network menu with a button for reenabling the type of network. + * @param {string} name The type of network (wifi, cellular or wimax). + * @param {string} command The command for reenabling the network. + * @param {string} type of icon (wifi or cellular). + * @private + */ + function addEnableNetworkButton_(name, command, icon) { + var subtitle = loadTimeData.getString('networkDisabled'); + var enableNetwork = function() { + chrome.send(command); + }; + var networkList = $('network-list'); + networkList.update({key: name, + subtitle: subtitle, + iconType: icon, + command: enableNetwork}); + } + + /** + * Element for indicating a policy managed network. + * @constructor + */ + function ManagedNetworkIndicator() { + var el = cr.doc.createElement('span'); + el.__proto__ = ManagedNetworkIndicator.prototype; + el.decorate(); + return el; + } + + ManagedNetworkIndicator.prototype = { + __proto__: ControlledSettingIndicator.prototype, + + /** @override */ + decorate: function() { + ControlledSettingIndicator.prototype.decorate.call(this); + this.controlledBy = 'policy'; + var policyLabel = loadTimeData.getString('managedNetwork'); + this.setAttribute('textPolicy', policyLabel); + this.removeAttribute('tabindex'); + }, + + /** @override */ + handleEvent: function(event) { + // Prevent focus blurring as that would close any currently open menu. + if (event.type == 'mousedown') + return; + ControlledSettingIndicator.prototype.handleEvent.call(this, event); + }, + + /** + * Handle mouse events received by the bubble, preventing focus blurring as + * that would close any currently open menu and preventing propagation to + * any elements located behind the bubble. + * @param {Event} Mouse event. + */ + stopEvent: function(event) { + event.preventDefault(); + event.stopPropagation(); + }, + + /** @override */ + toggleBubble_: function() { + if (activeMenu_ && !$(activeMenu_).contains(this)) + closeMenu_(); + ControlledSettingIndicator.prototype.toggleBubble_.call(this); + if (this.showingBubble) { + var bubble = OptionsPage.getVisibleBubble(); + bubble.addEventListener('mousedown', this.stopEvent); + bubble.addEventListener('click', this.stopEvent); + } + }, + }; + + /** + * Updates the list of available networks and their status, filtered by + * network type. + * @param {string} category The type of network. + * @param {Array} available The list of available networks and their status. + * @param {Array} remembered The list of remmebered networks. + */ + function loadData_(category, available, remembered) { + var data = {key: category}; + var type = categoryMap[category]; + var availableNetworks = []; + for (var i = 0; i < available.length; i++) { + if (available[i].networkType == type) + availableNetworks.push(available[i]); + } + data.networkList = availableNetworks; + if (remembered) { + var rememberedNetworks = []; + for (var i = 0; i < remembered.length; i++) { + if (remembered[i].networkType == type) + rememberedNetworks.push(remembered[i]); + } + data.rememberedNetworks = rememberedNetworks; + } + $('network-list').update(data); + } + + /** + * Hides the currently visible menu. + * @private + */ + function closeMenu_() { + if (activeMenu_) { + var menu = $(activeMenu_); + menu.hidden = true; + if (menu.data && menu.data.discardOnClose) + menu.parentNode.removeChild(menu); + activeMenu_ = null; + } + } + + /** + * Fetches the active connection. + * @param {Array.<Object>} networkList List of networks. + * @return {boolean} True if connected or connecting to a network. + * @private + */ + function getConnection_(networkList) { + if (!networkList) + return null; + for (var i = 0; i < networkList.length; i++) { + var entry = networkList[i]; + if (entry.connected || entry.connecting) + return entry; + } + return null; + } + + /** + * Create a callback function that adds a new connection of the given type. + * @param {!number} type A network type Constants.TYPE_*. + * @return {function()} The created callback. + * @private + */ + function createAddConnectionCallback_(type) { + return function() { + chrome.send('networkCommand', [String(type), '', 'add']); + }; + } + + /** + * Whether the Network list is disabled. Only used for display purpose. + * @type {boolean} + */ + cr.defineProperty(NetworkList, 'disabled', cr.PropertyKind.BOOL_ATTR); + + // Export + return { + NetworkList: NetworkList + }; +}); diff --git a/chromium/chrome/browser/resources/options/chromeos/overscan_arrows.png b/chromium/chrome/browser/resources/options/chromeos/overscan_arrows.png Binary files differnew file mode 100644 index 00000000000..6f3e394a063 --- /dev/null +++ b/chromium/chrome/browser/resources/options/chromeos/overscan_arrows.png diff --git a/chromium/chrome/browser/resources/options/chromeos/overscan_arrows_2x.png b/chromium/chrome/browser/resources/options/chromeos/overscan_arrows_2x.png Binary files differnew file mode 100644 index 00000000000..10171c6ad69 --- /dev/null +++ b/chromium/chrome/browser/resources/options/chromeos/overscan_arrows_2x.png diff --git a/chromium/chrome/browser/resources/options/chromeos/overscan_shift.png b/chromium/chrome/browser/resources/options/chromeos/overscan_shift.png Binary files differnew file mode 100644 index 00000000000..7f4bef5dd70 --- /dev/null +++ b/chromium/chrome/browser/resources/options/chromeos/overscan_shift.png diff --git a/chromium/chrome/browser/resources/options/chromeos/overscan_shift_2x.png b/chromium/chrome/browser/resources/options/chromeos/overscan_shift_2x.png Binary files differnew file mode 100644 index 00000000000..7d23fdfd6d9 --- /dev/null +++ b/chromium/chrome/browser/resources/options/chromeos/overscan_shift_2x.png diff --git a/chromium/chrome/browser/resources/options/chromeos/overscan_shift_rtl.png b/chromium/chrome/browser/resources/options/chromeos/overscan_shift_rtl.png Binary files differnew file mode 100644 index 00000000000..a42d926e221 --- /dev/null +++ b/chromium/chrome/browser/resources/options/chromeos/overscan_shift_rtl.png diff --git a/chromium/chrome/browser/resources/options/chromeos/overscan_shift_rtl_2x.png b/chromium/chrome/browser/resources/options/chromeos/overscan_shift_rtl_2x.png Binary files differnew file mode 100644 index 00000000000..fb89e2f9b8f --- /dev/null +++ b/chromium/chrome/browser/resources/options/chromeos/overscan_shift_rtl_2x.png diff --git a/chromium/chrome/browser/resources/options/chromeos/pointer_overlay.css b/chromium/chrome/browser/resources/options/chromeos/pointer_overlay.css new file mode 100644 index 00000000000..9076ef1929c --- /dev/null +++ b/chromium/chrome/browser/resources/options/chromeos/pointer_overlay.css @@ -0,0 +1,11 @@ +/* Copyright (c) 2012 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ + +#pointer-overlay > .content-area > :not([hidden]) + :nth-child(2) { + margin-top: 20px; +} + +#touchpad-scroll-direction { + margin-top: 12px; +} diff --git a/chromium/chrome/browser/resources/options/chromeos/pointer_overlay.html b/chromium/chrome/browser/resources/options/chromeos/pointer_overlay.html new file mode 100644 index 00000000000..562522e3e05 --- /dev/null +++ b/chromium/chrome/browser/resources/options/chromeos/pointer_overlay.html @@ -0,0 +1,50 @@ +<div id="pointer-overlay" class="page" hidden> + <div class="close-button"></div> + <div class="content-area"> + <section id="pointer-section-touchpad" hidden> + <h3 i18n-content="pointerOverlaySectionTitleTouchpad"></h3> + <div class="checkbox"> + <label> + <input type="checkbox" metric="Options_TouchpadTapToClick" + pref="settings.touchpad.enable_tap_to_click" dialog-pref> + <span i18n-content="enableTapToClick"></span> + </label> + </div> + <div class="radio" id="touchpad-scroll-direction"> + <label> + <input type="radio" name="touchpad-scroll-direction" value="false" + metric="Options_TouchpadNaturalScroll" + pref="settings.touchpad.natural_scroll" dialog-pref> + <span i18n-content="traditionalScroll"></span> + </label> + </div> + <div class="radio"> + <label> + <input type="radio" name="touchpad-scroll-direction" value="true" + metric="Options_TouchpadNaturalScroll" + pref="settings.touchpad.natural_scroll" dialog-pref> + <span i18n-values=".innerHTML:naturalScroll"></span> + </label> + </div> + </section> + <section id="pointer-section-mouse" hidden> + <h3 i18n-content="pointerOverlaySectionTitleMouse"></h3> + <div class="checkbox"> + <label> + <input type="checkbox" metric="Options_MousePrimaryRight" + pref="settings.mouse.primary_right" dialog-pref> + <span i18n-content="primaryMouseRight"></span> + </label> + </div> + </section> + </div> + <div class="action-area"> + <div class="button-strip"> + <button id="pointer-overlay-cancel" type="reset" i18n-content="cancel"> + </button> + <button id="pointer-overlay-confirm" class="default-button" type="submit" + i18n-content="ok"> + </button> + </div> + </div> +</div> diff --git a/chromium/chrome/browser/resources/options/chromeos/pointer_overlay.js b/chromium/chrome/browser/resources/options/chromeos/pointer_overlay.js new file mode 100644 index 00000000000..d6a4de9177b --- /dev/null +++ b/chromium/chrome/browser/resources/options/chromeos/pointer_overlay.js @@ -0,0 +1,78 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + /** @const */ var SettingsDialog = options.SettingsDialog; + + /** + * PointerOverlay class + * Dialog that allows users to set pointer settings (touchpad/mouse). + * @extends {SettingsDialog} + */ + function PointerOverlay() { + // The title is updated dynamically in the setTitle method as pointer + // devices are discovered or removed. + SettingsDialog.call(this, 'pointer-overlay', + '', 'pointer-overlay', + $('pointer-overlay-confirm'), + $('pointer-overlay-cancel')); + } + + cr.addSingletonGetter(PointerOverlay); + + PointerOverlay.prototype = { + __proto__: SettingsDialog.prototype, + + /** + * Initialize the page. + */ + initializePage: function() { + // Call base class implementation to start preference initialization. + SettingsDialog.prototype.initializePage.call(this); + }, + }; + + /** + * Sets the visibility state of the touchpad group. + * @param {boolean} show True to show, false to hide. + */ + PointerOverlay.showTouchpadControls = function(show) { + $('pointer-section-touchpad').hidden = !show; + }; + + /** + * Sets the visibility state of the mouse group. + * @param {boolean} show True to show, false to hide. + */ + PointerOverlay.showMouseControls = function(show) { + $('pointer-section-mouse').hidden = !show; + }; + + /** + * Updates the title of the pointer dialog. The title is set dynamically + * based on whether a touchpad, mouse or both are present. The label on the + * button that activates the overlay is also updated to stay in sync. A + * message is displayed in the main settings page if no pointer devices are + * available. + * @param {string} label i18n key for the overlay title. + */ + PointerOverlay.setTitle = function(label) { + var button = $('pointer-settings-button'); + var noPointersLabel = $('no-pointing-devices'); + if (label.length > 0) { + var title = loadTimeData.getString(label); + button.textContent = title; + button.hidden = false; + noPointersLabel.hidden = true; + } else { + button.hidden = true; + noPointersLabel.hidden = false; + } + }; + + // Export + return { + PointerOverlay: PointerOverlay + }; +}); diff --git a/chromium/chrome/browser/resources/options/chromeos/preferred_networks.html b/chromium/chrome/browser/resources/options/chromeos/preferred_networks.html new file mode 100644 index 00000000000..47b0d120a8b --- /dev/null +++ b/chromium/chrome/browser/resources/options/chromeos/preferred_networks.html @@ -0,0 +1,17 @@ +<div id="preferredNetworksPage" class="page" hidden> + <div class="close-button"></div> + <h1 id="preferred-networks-page-title" + i18n-content="preferredNetworksPage"></h1> + <div class="content-area"> + <div class="settings-list"> + <list id="remembered-network-list"></list> + </div> + </div> + <div class="action-area"> + <div class="button-strip"> + <button id="preferred-networks-confirm" class="default-button" + i18n-content="done"> + </button> + </div> + </div> +</div> diff --git a/chromium/chrome/browser/resources/options/chromeos/preferred_networks.js b/chromium/chrome/browser/resources/options/chromeos/preferred_networks.js new file mode 100644 index 00000000000..b6b461be94c --- /dev/null +++ b/chromium/chrome/browser/resources/options/chromeos/preferred_networks.js @@ -0,0 +1,164 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + + var OptionsPage = options.OptionsPage; + var ArrayDataModel = cr.ui.ArrayDataModel; + var DeletableItem = options.DeletableItem; + var DeletableItemList = options.DeletableItemList; + + ///////////////////////////////////////////////////////////////////////////// + // NetworkPreferences class: + + /** + * Encapsulated handling of ChromeOS network preferences page. + * @constructor + */ + function PreferredNetworks(model) { + OptionsPage.call(this, + 'preferredNetworksPage', + null, + 'preferredNetworksPage'); + } + + cr.addSingletonGetter(PreferredNetworks); + + PreferredNetworks.prototype = { + __proto__: OptionsPage.prototype, + + /** + * Initializes the preferred networks page. + */ + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + PreferredNetworkList.decorate($('remembered-network-list')); + $('preferred-networks-confirm').onclick = + OptionsPage.closeOverlay.bind(OptionsPage); + }, + + update: function(rememberedNetworks) { + var list = $('remembered-network-list'); + list.clear(); + for (var i = 0; i < rememberedNetworks.length; i++) { + list.append(rememberedNetworks[i]); + } + list.redraw(); + } + + }; + + /** + * Creates a list entry for a remembered network. + * @param{{networkName: string, + networkType: string, + servicePath: string}} data + * Description of the network. + * @constructor + */ + function PreferredNetworkListItem(data) { + var el = cr.doc.createElement('div'); + el.__proto__ = PreferredNetworkListItem.prototype; + el.data = {}; + for (var key in data) + el.data[key] = data[key]; + el.decorate(); + return el; + } + + PreferredNetworkListItem.prototype = { + __proto__: DeletableItem.prototype, + + /** + * Description of the network. + * @type {{networkName: string, + * networkType: string, + * servicePath: string}} + */ + data: null, + + /** @override */ + decorate: function() { + DeletableItem.prototype.decorate.call(this); + var label = this.ownerDocument.createElement('div'); + label.textContent = this.data.networkName; + if (this.data.policyManaged) + this.deletable = false; + this.contentElement.appendChild(label); + } + }; + + /** + * Class for displaying a list of preferred networks. + * @constructor + * @extends {options.DeletableItemList} + */ + var PreferredNetworkList = cr.ui.define('list'); + + PreferredNetworkList.prototype = { + __proto__: DeletableItemList.prototype, + + /** @override */ + decorate: function() { + DeletableItemList.prototype.decorate.call(this); + this.addEventListener('blur', this.onBlur_); + this.clear(); + }, + + /** + * When the list loses focus, unselect all items in the list. + * @private + */ + onBlur_: function() { + this.selectionModel.unselectAll(); + }, + + /** @override */ + createItem: function(entry) { + return new PreferredNetworkListItem(entry); + }, + + /** @override */ + deleteItemAtIndex: function(index) { + var item = this.dataModel.item(index); + if (item) { + // Inform the network library that we are forgetting this network. + chrome.send('networkCommand', + [item.networkType, + item.servicePath, + 'forget']); + } + this.dataModel.splice(index, 1); + // Invalidate the list since it has a stale cache after a splice + // involving a deletion. + this.invalidate(); + this.redraw(); + }, + + /** + * Purges all networks from the list. + */ + clear: function() { + this.dataModel = new ArrayDataModel([]); + this.redraw(); + }, + + /** + * Adds a remembered network to the list. + * @param {{networkName: string, + networkType: string, + servicePath: string} data + * Description of the network. + */ + append: function(data) { + this.dataModel.push(data); + } + }; + + // Export + return { + PreferredNetworks: PreferredNetworks + }; + +}); diff --git a/chromium/chrome/browser/resources/options/chromeos/proxy_rules_list.js b/chromium/chrome/browser/resources/options/chromeos/proxy_rules_list.js new file mode 100644 index 00000000000..1e65b4c0200 --- /dev/null +++ b/chromium/chrome/browser/resources/options/chromeos/proxy_rules_list.js @@ -0,0 +1,140 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options.proxyexceptions', function() { + /** @const */ var List = cr.ui.List; + /** @const */ var ListItem = cr.ui.ListItem; + /** @const */ var ArrayDataModel = cr.ui.ArrayDataModel; + + /** + * Creates a new exception list. + * @param {Object=} opt_propertyBag Optional properties. + * @constructor + * @extends {cr.ui.List} + */ + var ProxyExceptions = cr.ui.define('list'); + + ProxyExceptions.prototype = { + __proto__: List.prototype, + + pref: 'cros.session.proxy.ignorelist', + + /** @override */ + decorate: function() { + List.prototype.decorate.call(this); + this.autoExpands = true; + + // HACK(arv): http://crbug.com/40902 + window.addEventListener('resize', this.redraw.bind(this)); + + this.addEventListener('click', this.handleClick_); + + var self = this; + + // Listens to pref changes. + Preferences.getInstance().addEventListener(this.pref, + function(event) { + self.load_(event.value.value); + }); + }, + + createItem: function(exception) { + return new ProxyExceptionsItem(exception); + }, + + /** + * Adds given exception to model and update backend. + * @param {Object} exception A exception to be added to exception list. + */ + addException: function(exception) { + this.dataModel.push(exception); + this.updateBackend_(); + }, + + /** + * Removes given exception from model and update backend. + */ + removeException: function(exception) { + var dataModel = this.dataModel; + + var index = dataModel.indexOf(exception); + if (index >= 0) { + dataModel.splice(index, 1); + this.updateBackend_(); + } + }, + + /** + * Handles the clicks on the list and triggers exception removal if the + * click is on the remove exception button. + * @private + * @param {!Event} e The click event object. + */ + handleClick_: function(e) { + // Handle left button click + if (e.button == 0) { + var el = e.target; + if (el.className == 'remove-exception-button') { + this.removeException(el.parentNode.exception); + } + } + }, + + /** + * Loads given exception list. + * @param {Array} exceptions An array of exception object. + */ + load_: function(exceptions) { + this.dataModel = new ArrayDataModel(exceptions); + }, + + /** + * Updates backend. + */ + updateBackend_: function() { + Preferences.setListPref(this.pref, this.dataModel.slice(), true); + } + }; + + /** + * Creates a new exception list item. + * @param {Object} exception The exception account this represents. + * @constructor + * @extends {cr.ui.ListItem} + */ + function ProxyExceptionsItem(exception) { + var el = cr.doc.createElement('div'); + el.exception = exception; + ProxyExceptionsItem.decorate(el); + return el; + } + + /** + * Decorates an element as a exception account item. + * @param {!HTMLElement} el The element to decorate. + */ + ProxyExceptionsItem.decorate = function(el) { + el.__proto__ = ProxyExceptionsItem.prototype; + el.decorate(); + }; + + ProxyExceptionsItem.prototype = { + __proto__: ListItem.prototype, + + /** @override */ + decorate: function() { + ListItem.prototype.decorate.call(this); + this.className = 'exception-list-item'; + + var labelException = this.ownerDocument.createElement('span'); + labelException.className = ''; + labelException.textContent = this.exception; + this.appendChild(labelException); + } + }; + + return { + ProxyExceptions: ProxyExceptions + }; +}); diff --git a/chromium/chrome/browser/resources/options/chromeos/warning.png b/chromium/chrome/browser/resources/options/chromeos/warning.png Binary files differnew file mode 100644 index 00000000000..53713ba3f90 --- /dev/null +++ b/chromium/chrome/browser/resources/options/chromeos/warning.png diff --git a/chromium/chrome/browser/resources/options/clear_browser_data_overlay.css b/chromium/chrome/browser/resources/options/clear_browser_data_overlay.css new file mode 100644 index 00000000000..171d42a415b --- /dev/null +++ b/chromium/chrome/browser/resources/options/clear_browser_data_overlay.css @@ -0,0 +1,39 @@ +/* Copyright (c) 2012 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ + +#clear-browser-data-overlay { + width: 500px; +} + +#clear-data-checkboxes { + -webkit-padding-start: 8px; + margin: 5px 0; +} + +#cbd-throbber { + margin: 4px 10px; + vertical-align: middle; + visibility: hidden; +} + +#flash-storage-settings { + padding-top: 5px; +} + +#clear-browser-data-info-banner { + background-color: rgb(249, 237, 184); + border: solid 1px rgb(237, 201, 103); + border-radius: 2px; + font-weight: bold; + margin: 0 20px 11px 17px; + padding: 6px 8px; +} + +#some-stuff-remains-footer > p { + margin: 0; +} + +#some-stuff-remains-footer button { + padding: 0; +} diff --git a/chromium/chrome/browser/resources/options/clear_browser_data_overlay.html b/chromium/chrome/browser/resources/options/clear_browser_data_overlay.html new file mode 100644 index 00000000000..1debae7e6ef --- /dev/null +++ b/chromium/chrome/browser/resources/options/clear_browser_data_overlay.html @@ -0,0 +1,99 @@ +<div id="clear-browser-data-overlay" class="page" hidden> + <div class="close-button"></div> + <h1 i18n-content="clearBrowserDataOverlay"></h1> + <div id="clear-browser-data-info-banner" hidden> + <span i18n-content="clearBrowserDataInfoBar"></span> + </div> + <div id="cbd-content-area" class="content-area"> + <span i18n-content="clearBrowserDataLabel"></span> + <select id="clear-browser-data-time-period" + i18n-options="clearBrowserDataTimeList" + pref="browser.clear_data.time_period" + data-type="number"> + </select> + <div id="clear-data-checkboxes"> + <div id="delete-browsing-history-container" class="checkbox"> + <label> + <input id="delete-browsing-history-checkbox" + pref="browser.clear_data.browsing_history" type="checkbox"> + <span i18n-content="deleteBrowsingHistoryCheckbox"></span> + </label> + </div> + <div id="delete-download-history-container" class="checkbox"> + <label> + <input id="delete-download-history-checkbox" + pref="browser.clear_data.download_history" type="checkbox"> + <span i18n-content="deleteDownloadHistoryCheckbox"></span> + </label> + </div> + <div id="delete-cookies-container" class="checkbox"> + <label> + <input id="delete-cookies-checkbox" + pref="browser.clear_data.cookies" type="checkbox"> + <span i18n-content="deleteCookiesFlashCheckbox" + class="clear-plugin-lso-data-enabled"></span> + <span i18n-content="deleteCookiesCheckbox" + class="clear-plugin-lso-data-disabled"></span> + </label> + </div> + <div id="delete-cache-container" class="checkbox"> + <label> + <input id="delete-cache-checkbox" + pref="browser.clear_data.cache" type="checkbox"> + <span i18n-content="deleteCacheCheckbox"></span> + </label> + </div> + <div id="delete-passwords-container" class="checkbox"> + <label> + <input id="delete-passwords-checkbox" + pref="browser.clear_data.passwords" type="checkbox"> + <span i18n-content="deletePasswordsCheckbox"></span> + </label> + </div> + <div id="delete-form-data-container" class="checkbox"> + <label> + <input id="delete-form-data-checkbox" + pref="browser.clear_data.form_data" type="checkbox"> + <span i18n-content="deleteFormDataCheckbox"></span> + </label> + </div> + <div id="delete-hosted-apps-data-container" class="checkbox"> + <label> + <input id="delete-hosted-apps-data-checkbox" + pref="browser.clear_data.hosted_apps_data" type="checkbox"> + <span i18n-content="deleteHostedAppsDataCheckbox"></span> + </label> + </div> + <div id="deauthorize-content-licenses-container" + class="checkbox pepper-flash-settings"> + <label> + <input id="deauthorize-content-licenses-checkbox" + pref="browser.clear_data.content_licenses" type="checkbox"> + <span i18n-content="deauthorizeContentLicensesCheckbox"></span> + </label> + </div> + </div> + <div id="flash-storage-settings" class="flash-plugin-area"> + <a target="_blank" i18n-content="flash_storage_settings" + i18n-values="href:flash_storage_url"></a> + </div> + </div> + <div class="action-area"> + <div class="hbox stretch"> + <a target="_blank" i18n-content="learnMore" + i18n-values="href:clearBrowsingDataLearnMoreUrl"></a> + </div> + <div class="action-area-right"> + <div id="cbd-throbber" class="throbber"></div> + <div class="button-strip"> + <button id="clear-browser-data-dismiss" i18n-content="cancel"></button> + <button id="clear-browser-data-commit" class="default-button" + i18n-content="clearBrowserDataCommit"> + </button> + </div> + </div> + </div> + <div id="some-stuff-remains-footer" class="gray-bottom-bar"> + <p><!--This is filled by JavaScript--></p> + </div> +</div> diff --git a/chromium/chrome/browser/resources/options/clear_browser_data_overlay.js b/chromium/chrome/browser/resources/options/clear_browser_data_overlay.js new file mode 100644 index 00000000000..563a41a88ff --- /dev/null +++ b/chromium/chrome/browser/resources/options/clear_browser_data_overlay.js @@ -0,0 +1,209 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + var OptionsPage = options.OptionsPage; + + /** + * ClearBrowserDataOverlay class + * Encapsulated handling of the 'Clear Browser Data' overlay page. + * @class + */ + function ClearBrowserDataOverlay() { + OptionsPage.call(this, 'clearBrowserData', + loadTimeData.getString('clearBrowserDataOverlayTabTitle'), + 'clear-browser-data-overlay'); + } + + cr.addSingletonGetter(ClearBrowserDataOverlay); + + ClearBrowserDataOverlay.prototype = { + // Inherit ClearBrowserDataOverlay from OptionsPage. + __proto__: OptionsPage.prototype, + + // Whether deleting history and downloads is allowed. + allowDeletingHistory_: true, + + /** + * Initialize the page. + */ + initializePage: function() { + // Call base class implementation to starts preference initialization. + OptionsPage.prototype.initializePage.call(this); + + var f = this.updateCommitButtonState_.bind(this); + var types = ['browser.clear_data.browsing_history', + 'browser.clear_data.download_history', + 'browser.clear_data.cache', + 'browser.clear_data.cookies', + 'browser.clear_data.passwords', + 'browser.clear_data.form_data', + 'browser.clear_data.hosted_apps_data', + 'browser.clear_data.content_licenses']; + types.forEach(function(type) { + Preferences.getInstance().addEventListener(type, f); + }); + + var checkboxes = document.querySelectorAll( + '#cbd-content-area input[type=checkbox]'); + for (var i = 0; i < checkboxes.length; i++) { + checkboxes[i].onclick = f; + } + this.updateCommitButtonState_(); + + this.createStuffRemainsFooter_(); + + $('clear-browser-data-dismiss').onclick = function(event) { + ClearBrowserDataOverlay.dismiss(); + }; + $('clear-browser-data-commit').onclick = function(event) { + ClearBrowserDataOverlay.setClearingState(true); + chrome.send('performClearBrowserData'); + }; + + var show = loadTimeData.getBoolean('showDeleteBrowsingHistoryCheckboxes'); + this.showDeleteHistoryCheckboxes_(show); + }, + + // Create a footer that explains that some content is not cleared by the + // clear browsing history dialog. + createStuffRemainsFooter_: function() { + // The localized string is of the form "Saved [content settings] and + // {search engines} will not be cleared and may reflect your browsing + // habits.". The following parses out the parts in brackts and braces and + // converts them into buttons whereas the remainders are represented as + // span elements. + var footer = + document.querySelector('#some-stuff-remains-footer p'); + var footerFragments = + loadTimeData.getString('contentSettingsAndSearchEnginesRemain') + .split(/([|#])/); + for (var i = 0; i < footerFragments.length;) { + var buttonId = ''; + if (i + 2 < footerFragments.length) { + if (footerFragments[i] == '|' && footerFragments[i + 2] == '|') { + buttonId = 'open-content-settings-from-clear-browsing-data'; + } else if (footerFragments[i] == '#' && + footerFragments[i + 2] == '#') { + buttonId = 'open-search-engines-from-clear-browsing-data'; + } + } + + if (buttonId != '') { + var button = document.createElement('button'); + button.setAttribute('id', buttonId); + button.setAttribute('class', 'link-button'); + button.textContent = footerFragments[i + 1]; + footer.appendChild(button); + i += 3; + } else { + var span = document.createElement('span'); + span.textContent = footerFragments[i]; + footer.appendChild(span); + i += 1; + } + } + $('open-content-settings-from-clear-browsing-data').onclick = + function(event) { + OptionsPage.navigateToPage('content'); + } + $('open-search-engines-from-clear-browsing-data').onclick = + function(event) { + OptionsPage.navigateToPage('searchEngines'); + } + }, + + // Set the enabled state of the commit button. + updateCommitButtonState_: function() { + var checkboxes = document.querySelectorAll( + '#cbd-content-area input[type=checkbox]'); + var isChecked = false; + for (var i = 0; i < checkboxes.length; i++) { + if (checkboxes[i].checked) { + isChecked = true; + break; + } + } + $('clear-browser-data-commit').disabled = !isChecked; + }, + + setAllowDeletingHistory: function(allowed) { + this.allowDeletingHistory_ = allowed; + }, + + showDeleteHistoryCheckboxes_: function(show) { + if (!show) { + $('delete-browsing-history-container').hidden = true; + $('delete-download-history-container').hidden = true; + } + }, + + /** @override */ + didShowPage: function() { + var allowed = ClearBrowserDataOverlay.getInstance().allowDeletingHistory_; + ClearBrowserDataOverlay.updateHistoryCheckboxes(allowed); + }, + }; + + // + // Chrome callbacks + // + /** + * Updates the disabled status of the browsing-history and downloads + * checkboxes, also unchecking them if they are disabled. This is called in + * response to a change in the corresponding preference. + */ + ClearBrowserDataOverlay.updateHistoryCheckboxes = function(allowed) { + $('delete-browsing-history-checkbox').disabled = !allowed; + $('delete-download-history-checkbox').disabled = !allowed; + if (!allowed) { + $('delete-browsing-history-checkbox').checked = false; + $('delete-download-history-checkbox').checked = false; + } + ClearBrowserDataOverlay.getInstance().setAllowDeletingHistory(allowed); + }; + + ClearBrowserDataOverlay.setClearingState = function(state) { + $('delete-browsing-history-checkbox').disabled = state; + $('delete-download-history-checkbox').disabled = state; + $('delete-cache-checkbox').disabled = state; + $('delete-cookies-checkbox').disabled = state; + $('delete-passwords-checkbox').disabled = state; + $('delete-form-data-checkbox').disabled = state; + $('delete-hosted-apps-data-checkbox').disabled = state; + $('deauthorize-content-licenses-checkbox').disabled = state; + $('clear-browser-data-time-period').disabled = state; + $('cbd-throbber').style.visibility = state ? 'visible' : 'hidden'; + $('clear-browser-data-dismiss').disabled = state; + + if (state) + $('clear-browser-data-commit').disabled = true; + else + ClearBrowserDataOverlay.getInstance().updateCommitButtonState_(); + }; + + ClearBrowserDataOverlay.setBannerVisibility = function(args) { + var visible = args[0]; + $('clear-browser-data-info-banner').hidden = !visible; + }; + + ClearBrowserDataOverlay.doneClearing = function() { + // The delay gives the user some feedback that the clearing + // actually worked. Otherwise the dialog just vanishes instantly in most + // cases. + window.setTimeout(function() { + ClearBrowserDataOverlay.dismiss(); + }, 200); + }; + + ClearBrowserDataOverlay.dismiss = function() { + OptionsPage.closeOverlay(); + this.setClearingState(false); + }; + + // Export + return { + ClearBrowserDataOverlay: ClearBrowserDataOverlay + }; +}); diff --git a/chromium/chrome/browser/resources/options/confirm_dialog.js b/chromium/chrome/browser/resources/options/confirm_dialog.js new file mode 100644 index 00000000000..acf34e6db02 --- /dev/null +++ b/chromium/chrome/browser/resources/options/confirm_dialog.js @@ -0,0 +1,118 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + /** @const */ var OptionsPage = options.OptionsPage; + + /** + * A dialog that will pop up when the user attempts to set the value of the + * Boolean |pref| to |true|, asking for confirmation. If the user clicks OK, + * the new value is committed to Chrome. If the user clicks Cancel or leaves + * the settings page, the new value is discarded. + * @constructor + * @param {string} name See OptionsPage constructor. + * @param {string} title See OptionsPage constructor. + * @param {string} pageDivName See OptionsPage constructor. + * @param {HTMLInputElement} okButton The confirmation button element. + * @param {HTMLInputElement} cancelButton The cancellation button element. + * @param {string} pref The pref that requires confirmation. + * @param {string} metric User metrics identifier. + * @param {string} confirmed_pref A pref used to remember whether the user has + * confirmed the dialog before. This ensures that the user is presented + * with the dialog only once. If left |undefined| or |null|, the dialog + * will pop up every time the user attempts to set |pref| to |true|. + * @extends {OptionsPage} + */ + function ConfirmDialog(name, title, pageDivName, okButton, cancelButton, pref, + metric, confirmed_pref) { + OptionsPage.call(this, name, title, pageDivName); + this.okButton = okButton; + this.cancelButton = cancelButton; + this.pref = pref; + this.metric = metric; + this.confirmed_pref = confirmed_pref; + this.confirmed_ = false; + } + + ConfirmDialog.prototype = { + // Set up the prototype chain + __proto__: OptionsPage.prototype, + + /** + * Handle changes to |pref|. Only uncommitted changes are relevant as these + * originate from user and need to be explicitly committed to take effect. + * Pop up the dialog or commit the change, depending on whether confirmation + * is needed. + * @param {Event} event Change event. + * @private + */ + onPrefChanged_: function(event) { + if (!event.value.uncommitted) + return; + + if (event.value.value && !this.confirmed_) + OptionsPage.showPageByName(this.name, false); + else + Preferences.getInstance().commitPref(this.pref, this.metric); + }, + + /** + * Handle changes to |confirmed_pref| by caching them. + * @param {Event} event Change event. + * @private + */ + onConfirmedChanged_: function(event) { + this.confirmed_ = event.value.value; + }, + + /** @override */ + initializePage: function() { + this.okButton.onclick = this.handleConfirm.bind(this); + this.cancelButton.onclick = this.handleCancel.bind(this); + Preferences.getInstance().addEventListener( + this.pref, this.onPrefChanged_.bind(this)); + if (this.confirmed_pref) { + Preferences.getInstance().addEventListener( + this.confirmed_pref, this.onConfirmedChanged_.bind(this)); + } + }, + + /** + * Handle the confirm button by committing the |pref| change. If + * |confirmed_pref| has been specified, also remember that the dialog has + * been confirmed to avoid bringing it up in the future. + */ + handleConfirm: function() { + OptionsPage.closeOverlay(); + + Preferences.getInstance().commitPref(this.pref, this.metric); + if (this.confirmed_pref) + Preferences.setBooleanPref(this.confirmed_pref, true, true); + }, + + /** + * Handle the cancel button by rolling back the |pref| change without it + * ever taking effect. + */ + handleCancel: function() { + OptionsPage.closeOverlay(); + + Preferences.getInstance().rollbackPref(this.pref); + }, + + /** + * When a user navigates away from a confirm dialog, treat as a cancel. + * @protected + * @override + */ + willHidePage: function() { + if (this.visible) + Preferences.getInstance().rollbackPref(this.pref); + }, + }; + + return { + ConfirmDialog: ConfirmDialog + }; +}); diff --git a/chromium/chrome/browser/resources/options/content_settings.css b/chromium/chrome/browser/resources/options/content_settings.css new file mode 100644 index 00000000000..30e01d197fc --- /dev/null +++ b/chromium/chrome/browser/resources/options/content_settings.css @@ -0,0 +1,124 @@ +/* Copyright (c) 2012 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ + +#content-settings-page { + min-width: 620px; +} + +#content-settings-exceptions-area { + min-width: 540px; +} + +.exception-pattern { + -webkit-box-flex: 1; + -webkit-margin-end: 10px; + -webkit-margin-start: 14px; +} + +.exception-setting { + display: inline-block; + width: 120px; +} + +select.exception-setting { + vertical-align: middle; +} + +#exception-column-headers { + -webkit-margin-start: 17px; + display: -webkit-box; + margin-top: 17px; +} + +#exception-column-headers > div { + font-weight: bold; +} + +#exception-pattern-column { + -webkit-box-flex: 1; +} + +#exception-behavior-column { + width: 145px; +} + +.otr-explanation { + font-style: italic; +} + +#content-settings-exceptions-area list { + margin-bottom: 10px; + margin-top: 4px; +} + +#disable-plugins-container { + /* Same as .checkbox and .radio padding. Using padding instead of margin + * to ensure minimum height for tap target. */ + padding: 7px 0; +} + +#group-indicator { + margin-left: 5px; +} + +div[role='listitem'][controlled-by] { + color: #666; + font-style: italic; + position: relative; +} + +.section-header { + -webkit-margin-start: -18px; + margin-bottom: 0.8em; +} + +.section-header > h3 { + display: inline; +} + +.settings-list div[role='listitem'][controlled-by='policy'], +.settings-list div[role='listitem'][controlled-by='extension'] { + background: rgb(250, 230, 146); + border-bottom: 1px solid rgb(201, 189, 141); + border-top: 0; +} + +.sublabel { + -webkit-margin-start: 2em; +} + +.media-device-control { + display: table-row; +} + +.media-device-control > span { + display: table-cell; + min-width: 145px; +} + +.media-device-control > select { + display: table-cell; + margin: 5px 0; + width: 12em; +} + +.exception-setting.media-audio-setting { + width: 6em; +} + +.exception-setting.media-video-setting { + width: 6.5em; +} + +#media-column-header { + display: -webkit-box; +} + +#media-audio-column { + width: 6em; +} + +#media-video-column { + width: 8.5em; +} diff --git a/chromium/chrome/browser/resources/options/content_settings.html b/chromium/chrome/browser/resources/options/content_settings.html new file mode 100644 index 00000000000..5cebc041b44 --- /dev/null +++ b/chromium/chrome/browser/resources/options/content_settings.html @@ -0,0 +1,601 @@ +<div id="content-settings-page" class="page" hidden> + <div class="close-button"></div> + <h1 i18n-content="contentSettingsPage"></h1> + <div class="content-area"> + <!-- Cookie filter tab contents --> + <section> + <h3 i18n-content="cookies_tab_label"></h3> + <div> + <div class="radio"> + <span class="controlled-setting-with-label"> + <input id="cookies-allow" type="radio" name="cookies" value="allow"> + <span> + <label for="cookies-allow" i18n-content="cookies_allow"></label> + <span class="controlled-setting-indicator" + content-setting="cookies" value="allow"> + </span> + </span> + </span> + </div> + <div class="radio"> + <span class="controlled-setting-with-label"> + <input id="cookies-session" type="radio" name="cookies" + value="session"> + <span> + <label for="cookies-session" i18n-content="cookies_session_only"> + </label> + <span class="controlled-setting-indicator" + content-setting="cookies" value="session"> + </span> + </span> + </span> + </div> + <div class="radio"> + <span class="controlled-setting-with-label"> + <input id="cookies-block" type="radio" name="cookies" value="block"> + <span> + <label for="cookies-block" i18n-content="cookies_block"></label> + <span class="controlled-setting-indicator" + content-setting="cookies" value="block"> + </span> + </span> + </span> + </div> + <div class="checkbox"> + <span class="controlled-setting-with-label"> + <input id="block-third-party-cookies" + pref="profile.block_third_party_cookies" type="checkbox"> + <span> + <label for="block-third-party-cookies" + i18n-content="cookies_block_3rd_party"> + </label> + <span class="controlled-setting-indicator" + pref="profile.block_third_party_cookies"> + </span> + </span> + </span> + </div> + <!-- TODO(jochen): remove the div with the clear cookies on exit option + once this has shipped. --> + <div class="checkbox" guest-visibility="disabled" hidden> + <label> + <input id="clear-cookies-on-exit" + pref="profile.clear_site_data_on_exit" type="checkbox"> + <span i18n-content="cookies_lso_clear_when_close" + class="clear-plugin-lso-data-enabled"></span> + <span i18n-content="cookies_clear_when_close" + class="clear-plugin-lso-data-disabled"></span> + </label> + </div> + <div class="settings-row"> + <button class="exceptions-list-button" contentType="cookies" + i18n-content="manageExceptions"></button> + <button id="show-cookies-button" + i18n-content="cookies_show_cookies"></button> + </div> + </div> + </section> + <!-- Image filter --> + <section> + <h3 i18n-content="images_tab_label"></h3> + <div> + <div class="radio"> + <span class="controlled-setting-with-label"> + <input id="images-allow" type="radio" name="images" value="allow"> + <span> + <label for="images-allow" i18n-content="images_allow"></label> + <span class="controlled-setting-indicator" + content-setting="images" value="allow"> + </span> + </span> + </span> + </div> + <div class="radio"> + <span class="controlled-setting-with-label"> + <input id="images-block" type="radio" name="images" value="block"> + <span> + <label for="images-block" i18n-content="images_block"></label> + <span class="controlled-setting-indicator" + content-setting="images" value="block"> + </span> + </span> + </span> + </div> + <div class="settings-row"> + <button class="exceptions-list-button" contentType="images" + i18n-content="manageExceptions"></button> + </div> + </div> + </section> + <!-- JavaScript filter --> + <section> + <h3 i18n-content="javascript_tab_label"></h3> + <div> + <div class="radio"> + <span class="controlled-setting-with-label"> + <input id="javascript-allow" type="radio" name="javascript" + value="allow"> + <span> + <label for="javascript-allow" i18n-content="javascript_allow"> + </label> + <span class="controlled-setting-indicator" + content-setting="javascript" value="allow"> + </span> + </span> + </span> + </div> + <div class="radio"> + <span class="controlled-setting-with-label"> + <input id="javascript-block" type="radio" name="javascript" + value="block"> + <span> + <label for="javascript-block" i18n-content="javascript_block"> + </label> + <span class="controlled-setting-indicator" + content-setting="javascript" value="block"> + </span> + </span> + </span> + </div> + <div class="settings-row"> + <button class="exceptions-list-button" contentType="javascript" + i18n-content="manageExceptions"></button> + </div> + </div> + </section> + <!-- Handlers settings --> + <section id="handlers-section"> + <h3 i18n-content="handlers_tab_label"></h3> + <div> + <div class="radio"> + <label> + <input type="radio" name="handlers" value="allow" + class="handler-radio"> + <span i18n-content="handlers_allow"></span> + </label> + </div> + <div class="radio"> + <label> + <input type="radio" name="handlers" value="block" + class="handler-radio"> + <span i18n-content="handlers_block"></span> + </label> + </div> + <div class="settings-row"> + <button id="manage-handlers-button" contentType="handlers" + i18n-content="manage_handlers"></button> + </div> + </div> + </section> + <!-- Plug-ins filter --> + <section> + <h3 i18n-content="plugins_tab_label"></h3> + <div> + <div class="radio"> + <span class="controlled-setting-with-label"> + <input id="plugins-allow" type="radio" name="plugins" value="allow"> + <span> + <label for="plugins-allow" i18n-content="plugins_allow"></label> + <span class="controlled-setting-indicator" + content-setting="plugins" value="allow"> + </span> + </span> + </span> + </div> + <div id="click_to_play" class="radio"> + <span class="controlled-setting-with-label"> + <input id="plugins-ask" type="radio" name="plugins" value="ask"> + <span> + <label for="plugins-ask" i18n-content="plugins_ask"></label> + <span class="controlled-setting-indicator" + content-setting="plugins" value="ask"> + </span> + </span> + </span> + </div> + <div class="radio"> + <span class="controlled-setting-with-label"> + <input id="plugins-block" type="radio" name="plugins" value="block"> + <span> + <label for="plugins-block" i18n-content="plugins_block"></label> + <span class="controlled-setting-indicator" + content-setting="plugins" value="block"> + </span> + </span> + </span> + </div> + <div class="settings-row"> + <button class="exceptions-list-button" contentType="plugins" + i18n-content="manageExceptions"></button> + </div> + <div id="disable-plugins-container"> + <a href="chrome://plugins" i18n-content="disableIndividualPlugins" + target="_blank"></a> + </div> + </div> + </section> + <!-- Pop-ups filter --> + <section> + <h3 i18n-content="popups_tab_label" class="content-settings-header"></h3> + <div> + <div class="radio"> + <span class="controlled-setting-with-label"> + <input id="popups-allow" type="radio" name="popups" value="allow"> + <span> + <label for="popups-allow" i18n-content="popups_allow"></label> + <span class="controlled-setting-indicator" + content-setting="popups" value="allow"> + </span> + </span> + </span> + </div> + <div class="radio"> + <span class="controlled-setting-with-label"> + <input id="popups-block" type="radio" name="popups" value="block"> + <span> + <label for="popups-block" i18n-content="popups_block"></label> + <span class="controlled-setting-indicator" + content-setting="popups" value="block"> + </span> + </span> + </span> + </div> + <div class="settings-row"> + <button class="exceptions-list-button" contentType="popups" + i18n-content="manageExceptions"></button> + </div> + </div> + </section> + <!-- Location filter --> + <section> + <h3 i18n-content="location_tab_label"></h3> + <div> + <div class="radio"> + <span class="controlled-setting-with-label"> + <input id="location-allow" type="radio" name="location" + value="allow"> + <span> + <label for="location-allow" i18n-content="location_allow"></label> + <span class="controlled-setting-indicator" + content-setting="location" value="allow"> + </span> + </span> + </span> + </div> + <div class="radio"> + <span class="controlled-setting-with-label"> + <input id="location-ask" type="radio" name="location" value="ask"> + <span> + <label for="location-ask" i18n-content="location_ask"></label> + <span class="controlled-setting-indicator" + content-setting="location" value="ask"> + </span> + </span> + </span> + </div> + <div class="radio"> + <span class="controlled-setting-with-label"> + <input id="location-block" type="radio" name="location" + value="block"> + <span> + <label for="location-block" i18n-content="location_block"></label> + <span class="controlled-setting-indicator" + content-setting="location" value="block"> + </span> + </span> + </span> + </div> +<if expr="pp_ifdef('enable_google_now')"> + <div class="checkbox" id="geolocationCheckbox" hidden> + <span class="controlled-setting-with-label"> + <input id="googleGeolocationAccessEnabled" + pref="googlegeolocationaccess.enabled" + metric="Options_GoogleGeolocationAccessCheckbox" + type="checkbox"> + <span> + <label for="googleGeolocationAccessEnabled" + i18n-content="googleGeolocationAccessEnable"> + </label> + <span class="controlled-setting-indicator" + pref="googlegeolocationaccess.enabled"> + </span> + </span> + </span> + </div> +</if> + <div class="settings-row"> + <button class="exceptions-list-button" contentType="location" + i18n-content="manageExceptions"></button> + </div> + </div> + </section> + <!-- Notifications filter tab contents --> + <section id="notifications-section"> + <h3 i18n-content="notifications_tab_label"></h3> + <div> + <div class="radio"> + <span class="controlled-setting-with-label"> + <input id="notifications-allow" type="radio" name="notifications" + value="allow"> + <span> + <label for="notifications-allow" + i18n-content="notifications_allow"> + </label> + <span class="controlled-setting-indicator" + content-setting="notifications" value="allow"> + </span> + </span> + </span> + </div> + <div class="radio"> + <span class="controlled-setting-with-label"> + <input id="notifications-ask" type="radio" name="notifications" + value="ask"> + <span> + <label for="notifications-ask" i18n-content="notifications_ask"> + </label> + <span class="controlled-setting-indicator" + content-setting="notifications" value="ask"> + </span> + </span> + </span> + </div> + <div class="radio"> + <span class="controlled-setting-with-label"> + <input id="notifications-block" type="radio" name="notifications" + value="block"> + <span> + <label for="notifications-block" + i18n-content="notifications_block"> + </label> + <span class="controlled-setting-indicator" + content-setting="notifications" value="block"> + </span> + </span> + </span> + </div> + <div class="settings-row"> + <button class="exceptions-list-button" contentType="notifications" + i18n-content="manageExceptions"></button> + </div> + </div> + </section> + <!-- Fullscreen filter --> + <section> + <h3 i18n-content="fullscreen_tab_label"></h3> + <div class="settings-row"> + <button class="exceptions-list-button" contentType="fullscreen" + i18n-content="manageExceptions"></button> + </div> + </section> + <!-- Mouse Lock filter --> + <section> + <h3 i18n-content="mouselock_tab_label"></h3> + <div> + <div class="radio"> + <label> + <input type="radio" name="mouselock" value="allow"> + <span i18n-content="mouselock_allow"></span> + </label> + </div> + <div class="radio"> + <label> + <input type="radio" name="mouselock" value="ask"> + <span i18n-content="mouselock_ask"></span> + </label> + </div> + <div class="radio"> + <label> + <input type="radio" name="mouselock" value="block"> + <span i18n-content="mouselock_block"></span> + </label> + </div> + <div class="settings-row"> + <button class="exceptions-list-button" contentType="mouselock" + i18n-content="manageExceptions"></button> + </div> + </div> + </section> +<if expr="pp_ifdef('chromeos') or is_win"> + <!-- Protected Content filter --> + <section guest-visibility="disabled"> + <h3 i18n-content="protectedContentTabLabel" + class="content-settings-header"></h3> + <div> + <div class="settings-row"> + <p i18n-content="protectedContentInfo"></p> + </div> + <div class="checkbox"> + <label> + <input pref="settings.privacy.drm_enabled" type="checkbox"> + <span i18n-content="protectedContentEnable"></span> + </label> + </div> + <div class="settings-row"> + <button id="protected-content-exceptions" + class="exceptions-list-button" contentType="protectedContent" + i18n-content="manageExceptions"></button> + </div> + </div> + </section> +</if> + <!-- Media Stream capture device filter --> + <section> + <div class="section-header"> + <h3 i18n-content="mediaStreamTabLabel"></h3> + <span id="media-indicator" + class="controlled-setting-indicator group-indicator"></span> + </div> + <div> + <div class="media-device-control"> + <span i18n-content="mediaSelectMicLabel"></span> + <select id="media-select-mic" class="weakrtl"></select> + </div> + <div class="media-device-control"> + <span i18n-content="mediaSelectCameraLabel"></span> + <select id="media-select-camera" class="weakrtl"></select> + </div> + <div class="radio"> + <span class="controlled-setting-with-label"> + <input id="media-stream-ask" type="radio" name="media-stream" + value="ask"> + <span> + <label id="media-stream-ask-label" for="media-stream-ask" + i18n-content="mediaStreamAsk"> + </label> + <span class="controlled-setting-indicator" + content-setting="media-stream" value="ask"> + </span> + </span> + </span> + </div> + <div class="radio"> + <span class="controlled-setting-with-label"> + <input id="media-stream-block" type="radio" name="media-stream" + value="block"> + <span> + <label id="media-stream-block-label" for="media-stream-block" + i18n-content="mediaStreamBlock"> + </label> + <span class="controlled-setting-indicator" + content-setting="media-stream" value="block"> + </span> + </span> + </span> + </div> + <div class="settings-row"> + <button class="exceptions-list-button" contentType="media-stream" + i18n-content="manageExceptions"></button> + </div> + <div id="media-pepper-flash-default" class="pepper-flash-settings"> + <span i18n-content="mediaPepperFlashDefaultDivergedLabel"></span> + <a target="_blank" i18n-content="mediaPepperFlashChangeLink" + i18n-values="href:mediaPepperFlashGlobalPrivacyURL"></a> + </div> + </div> + </section> + <!-- PPAPI broker --> + <section> + <h3 i18n-content="ppapiBrokerTabLabel"></h3> + <div> + <div class="radio"> + <label> + <input type="radio" name="ppapi-broker" value="allow"> + <span i18n-content="ppapi_broker_allow"></span> + </label> + </div> + <div class="radio"> + <label> + <input type="radio" name="ppapi-broker" value="ask"> + <span i18n-content="ppapi_broker_ask"></span> + </label> + </div> + <div class="radio"> + <label> + <input type="radio" name="ppapi-broker" value="block"> + <span i18n-content="ppapi_broker_block"></span> + </label> + </div> + <div class="settings-row"> + <button class="exceptions-list-button" contentType="ppapi-broker" + i18n-content="manageExceptions"></button> + </div> + </div> + </section> + <section id="media-galleries-section" hidden> + <h3 i18n-content="mediaGalleriesSectionLabel"></h3> + <div class="settings-row"> + <button id="manage-galleries-button" + i18n-content="manageGalleriesButton"></button> + </div> + </section> + <!-- Automatic Downloads filter --> + <section> + <h3 i18n-content="multiple-automatic-downloads_header"></h3> + <div> + <div class="radio"> + <span class="controlled-setting-with-label"> + <input id="multiple-automatic-downloads_allow" type="radio" + name="multiple-automatic-downloads" value="allow"> + <span> + <label for="multiple-automatic-downloads_allow" + i18n-content="multiple-automatic-downloads_allow"> + </label> + <span class="controlled-setting-indicator" + content-setting="multiple-automatic-downloads" value="allow"> + </span> + </span> + </span> + </div> + <div class="radio"> + <span class="controlled-setting-with-label"> + <input id="multiple-automatic-downloads_ask" type="radio" + name="multiple-automatic-downloads" value="ask"> + <span> + <label for="multiple-automatic-downloads_ask" + i18n-content="multiple-automatic-downloads_ask"> + </label> + <span class="controlled-setting-indicator" + content-setting="multiple-automatic-downloads" value="ask"> + </span> + </span> + </span> + </div> + <div class="radio"> + <span class="controlled-setting-with-label"> + <input id="multiple-automatic-downloads_block" type="radio" + name="multiple-automatic-downloads" value="block"> + <span> + <label for="multiple-automatic-downloads_block" + i18n-content="multiple-automatic-downloads_block"> + </label> + <span class="controlled-setting-indicator" + content-setting="multiple-automatic-downloads" value="block"> + </span> + </span> + </span> + </div> + <div class="settings-row"> + <button class="exceptions-list-button" + contentType="multiple-automatic-downloads" + i18n-content="manageExceptions"></button> + </div> + </div> + </section> + <!-- MIDI system exclusive messages filter --> + <section id="experimental-web-midi-settings" hidden="true"> + <h3 i18n-content="midi-sysex_header"></h3> + <div> + <div class="radio"> + <label> + <input type="radio" name="midi-sysex" value="allow"> + <span i18n-content="midiSysExAllow"></span> + </label> + </div> + <div class="radio"> + <label> + <input type="radio" name="midi-sysex" value="ask"> + <span i18n-content="midiSysExAsk"></span> + </label> + </div> + <div class="radio"> + <label> + <input type="radio" name="midi-sysex" value="block"> + <span i18n-content="midiSysExBlock"></span> + </label> + </div> + <div class="settings-row"> + <button class="exceptions-list-button" contentType="midi-sysex" + i18n-content="manageExceptions"></button> + </div> + </div> + </section> + </div> + <div class="action-area"> + <div class="button-strip"> + <button id="content-settings-overlay-confirm" class="default-button" + i18n-content="done"> + </button> + </div> + </div> +</div> diff --git a/chromium/chrome/browser/resources/options/content_settings.js b/chromium/chrome/browser/resources/options/content_settings.js new file mode 100644 index 00000000000..160d36f6d79 --- /dev/null +++ b/chromium/chrome/browser/resources/options/content_settings.js @@ -0,0 +1,314 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +if (!loadTimeData.getBoolean('newContentSettings')) { + +cr.define('options', function() { + /** @const */ var OptionsPage = options.OptionsPage; + + ////////////////////////////////////////////////////////////////////////////// + // ContentSettings class: + + /** + * Encapsulated handling of content settings page. + * @constructor + */ + function ContentSettings() { + this.activeNavTab = null; + OptionsPage.call(this, 'content', + loadTimeData.getString('contentSettingsPageTabTitle'), + 'content-settings-page'); + } + + cr.addSingletonGetter(ContentSettings); + + ContentSettings.prototype = { + __proto__: OptionsPage.prototype, + + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + + var exceptionsButtons = + this.pageDiv.querySelectorAll('.exceptions-list-button'); + for (var i = 0; i < exceptionsButtons.length; i++) { + exceptionsButtons[i].onclick = function(event) { + var page = ContentSettingsExceptionsArea.getInstance(); + + // Add on the proper hash for the content type, and store that in the + // history so back/forward and tab restore works. + var hash = event.currentTarget.getAttribute('contentType'); + var url = page.name + '#' + hash; + window.history.pushState({pageName: page.name}, + page.title, + '/' + url); + + // Navigate after the history has been replaced in order to have the + // correct hash loaded. + OptionsPage.showPageByName('contentExceptions', false); + + uber.invokeMethodOnParent('setPath', {path: url}); + uber.invokeMethodOnParent('setTitle', + {title: loadTimeData.getString(hash + 'TabTitle')}); + }; + } + + var manageHandlersButton = $('manage-handlers-button'); + if (manageHandlersButton) { + manageHandlersButton.onclick = function(event) { + OptionsPage.navigateToPage('handlers'); + }; + } + + $('manage-galleries-button').onclick = function(event) { + OptionsPage.navigateToPage('manageGalleries'); + }; + + if (cr.isChromeOS) + UIAccountTweaks.applyGuestModeVisibility(document); + + // Cookies filter page --------------------------------------------------- + $('show-cookies-button').onclick = function(event) { + chrome.send('coreOptionsUserMetricsAction', ['Options_ShowCookies']); + OptionsPage.navigateToPage('cookies'); + }; + + $('content-settings-overlay-confirm').onclick = + OptionsPage.closeOverlay.bind(OptionsPage); + + $('media-pepper-flash-default').hidden = true; + $('media-pepper-flash-exceptions').hidden = true; + + $('media-select-mic').addEventListener('change', + ContentSettings.setDefaultMicrophone_); + $('media-select-camera').addEventListener('change', + ContentSettings.setDefaultCamera_); + }, + }; + + ContentSettings.updateHandlersEnabledRadios = function(enabled) { + var selector = '#content-settings-page input[type=radio][value=' + + (enabled ? 'allow' : 'block') + '].handler-radio'; + document.querySelector(selector).checked = true; + }; + + /** + * Sets the values for all the content settings radios. + * @param {Object} dict A mapping from radio groups to the checked value for + * that group. + */ + ContentSettings.setContentFilterSettingsValue = function(dict) { + for (var group in dict) { + var managedBy = dict[group].managedBy; + var controlledBy = managedBy == 'policy' || managedBy == 'extension' ? + managedBy : null; + document.querySelector('input[type=radio][name=' + group + '][value=' + + dict[group].value + ']').checked = true; + var radios = document.querySelectorAll('input[type=radio][name=' + + group + ']'); + for (var i = 0, len = radios.length; i < len; i++) { + radios[i].disabled = (managedBy != 'default'); + radios[i].controlledBy = controlledBy; + } + var indicators = document.querySelectorAll( + 'span.controlled-setting-indicator[content-setting=' + group + ']'); + if (indicators.length == 0) + continue; + // Create a synthetic pref change event decorated as + // CoreOptionsHandler::CreateValueForPref() does. + var event = new Event(group); + event.value = { + value: dict[group].value, + controlledBy: controlledBy, + }; + for (var i = 0; i < indicators.length; i++) + indicators[i].handlePrefChange(event); + } + }; + + /** + * Updates the labels and indicators for the Media settings. Those require + * special handling because they are backed by multiple prefs and can change + * their scope based on the managed state of the backing prefs. + * @param {Object} mediaSettings A dictionary containing the following fields: + * {String} askText The label for the ask radio button. + * {String} blockText The label for the block radio button. + * {Boolean} cameraDisabled Whether to disable the camera dropdown. + * {Boolean} micDisabled Whether to disable the microphone dropdown. + * {Boolean} showBubble Wether to show the managed icon and bubble for the + * media label. + * {String} bubbleText The text to use inside the bubble if it is shown. + */ + ContentSettings.updateMediaUI = function(mediaSettings) { + $('media-stream-ask-label').innerHTML = + loadTimeData.getString(mediaSettings.askText); + $('media-stream-block-label').innerHTML = + loadTimeData.getString(mediaSettings.blockText); + + if (mediaSettings.micDisabled) + $('media-select-mic').disabled = true; + if (mediaSettings.cameraDisabled) + $('media-select-camera').disabled = true; + + OptionsPage.hideBubble(); + // Create a synthetic pref change event decorated as + // CoreOptionsHandler::CreateValueForPref() does. + // TODO(arv): It was not clear what event type this should use? + var event = new Event('undefined'); + event.value = {}; + + if (mediaSettings.showBubble) { + event.value = { controlledBy: 'policy' }; + $('media-indicator').setAttribute( + 'textpolicy', loadTimeData.getString(mediaSettings.bubbleText)); + $('media-indicator').location = cr.ui.ArrowLocation.TOP_START; + } + + $('media-indicator').handlePrefChange(event); + }; + + /** + * Initializes an exceptions list. + * @param {string} type The content type that we are setting exceptions for. + * @param {Array} list An array of pairs, where the first element of each pair + * is the filter string, and the second is the setting (allow/block). + */ + ContentSettings.setExceptions = function(type, list) { + var exceptionsList = + document.querySelector('div[contentType=' + type + ']' + + ' list[mode=normal]'); + exceptionsList.setExceptions(list); + }; + + ContentSettings.setHandlers = function(list) { + $('handlers-list').setHandlers(list); + }; + + ContentSettings.setIgnoredHandlers = function(list) { + $('ignored-handlers-list').setHandlers(list); + }; + + ContentSettings.setOTRExceptions = function(type, list) { + var exceptionsList = + document.querySelector('div[contentType=' + type + ']' + + ' list[mode=otr]'); + + exceptionsList.parentNode.hidden = false; + exceptionsList.setExceptions(list); + }; + + /** + * The browser's response to a request to check the validity of a given URL + * pattern. + * @param {string} type The content type. + * @param {string} mode The browser mode. + * @param {string} pattern The pattern. + * @param {bool} valid Whether said pattern is valid in the context of + * a content exception setting. + */ + ContentSettings.patternValidityCheckComplete = + function(type, mode, pattern, valid) { + var exceptionsList = + document.querySelector('div[contentType=' + type + '] ' + + 'list[mode=' + mode + ']'); + exceptionsList.patternValidityCheckComplete(pattern, valid); + }; + + /** + * Shows/hides the link to the Pepper Flash camera and microphone default + * settings. + * Please note that whether the link is actually showed or not is also + * affected by the style class pepper-flash-settings. + */ + ContentSettings.showMediaPepperFlashDefaultLink = function(show) { + $('media-pepper-flash-default').hidden = !show; + } + + /** + * Shows/hides the link to the Pepper Flash camera and microphone + * site-specific settings. + * Please note that whether the link is actually showed or not is also + * affected by the style class pepper-flash-settings. + */ + ContentSettings.showMediaPepperFlashExceptionsLink = function(show) { + $('media-pepper-flash-exceptions').hidden = !show; + } + + /** + * Shows/hides the whole Web MIDI settings. + * @param {bool} show Wether to show the whole Web MIDI settings. + */ + ContentSettings.showExperimentalWebMIDISettings = function(show) { + $('experimental-web-midi-settings').hidden = !show; + } + + /** + * Updates the microphone/camera devices menu with the given entries. + * @param {string} type The device type. + * @param {Array} devices List of available devices. + * @param {string} defaultdevice The unique id of the current default device. + */ + ContentSettings.updateDevicesMenu = function(type, devices, defaultdevice) { + var deviceSelect = ''; + if (type == 'mic') { + deviceSelect = $('media-select-mic'); + } else if (type == 'camera') { + deviceSelect = $('media-select-camera'); + } else { + console.error('Unknown device type for <device select> UI element: ' + + type); + return; + } + + deviceSelect.textContent = ''; + + var deviceCount = devices.length; + var defaultIndex = -1; + for (var i = 0; i < deviceCount; i++) { + var device = devices[i]; + var option = new Option(device.name, device.id); + if (option.value == defaultdevice) + defaultIndex = i; + deviceSelect.appendChild(option); + } + if (defaultIndex >= 0) + deviceSelect.selectedIndex = defaultIndex; + }; + + /** + * Enables/disables the protected content exceptions button. + * @param {bool} enable Whether to enable the button. + */ + ContentSettings.enableProtectedContentExceptions = function(enable) { + var exceptionsButton = $('protected-content-exceptions'); + if (exceptionsButton) { + exceptionsButton.disabled = !enable; + } + } + + /** + * Set the default microphone device based on the popup selection. + * @private + */ + ContentSettings.setDefaultMicrophone_ = function() { + var deviceSelect = $('media-select-mic'); + chrome.send('setDefaultCaptureDevice', ['mic', deviceSelect.value]); + }; + + /** + * Set the default camera device based on the popup selection. + * @private + */ + ContentSettings.setDefaultCamera_ = function() { + var deviceSelect = $('media-select-camera'); + chrome.send('setDefaultCaptureDevice', ['camera', deviceSelect.value]); + }; + + // Export + return { + ContentSettings: ContentSettings + }; + +}); + +} diff --git a/chromium/chrome/browser/resources/options/content_settings2.html b/chromium/chrome/browser/resources/options/content_settings2.html new file mode 100644 index 00000000000..e886bfdeb78 --- /dev/null +++ b/chromium/chrome/browser/resources/options/content_settings2.html @@ -0,0 +1,13 @@ +<div id="content-settings-page2" class="page" hidden> + <div class="close-button"></div> + <h1 i18n-content="contentSettingsPage"></h1> + <div class="content-area"> + This space intentionally left blank. + </div> + <div class="action-area"> + <div class="button-strip"> + <button id="content-settings-overlay-confirm2" i18n-content="done"> + </button> + </div> + </div> +</div> diff --git a/chromium/chrome/browser/resources/options/content_settings2.js b/chromium/chrome/browser/resources/options/content_settings2.js new file mode 100644 index 00000000000..7c6d95137e3 --- /dev/null +++ b/chromium/chrome/browser/resources/options/content_settings2.js @@ -0,0 +1,88 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +if (loadTimeData.getBoolean('newContentSettings')) { + +cr.define('options', function() { + /** @const */ var OptionsPage = options.OptionsPage; + + ////////////////////////////////////////////////////////////////////////////// + // ContentSettings class: + + /** + * Encapsulated handling of content settings page. + * @constructor + */ + function ContentSettings() { + this.activeNavTab = null; + OptionsPage.call(this, 'content', + loadTimeData.getString('contentSettingsPageTabTitle'), + 'content-settings-page2'); + } + + cr.addSingletonGetter(ContentSettings); + + ContentSettings.prototype = { + __proto__: OptionsPage.prototype, + + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + + $('content-settings-overlay-confirm2').onclick = + OptionsPage.closeOverlay.bind(OptionsPage); + }, + }; + + ContentSettings.updateHandlersEnabledRadios = function(enabled) { + // Not implemented. + }; + + /** + * Sets the values for all the content settings radios. + * @param {Object} dict A mapping from radio groups to the checked value for + * that group. + */ + ContentSettings.setContentFilterSettingsValue = function(dict) { + // Not implemented. + }; + + /** + * Initializes an exceptions list. + * @param {string} type The content type that we are setting exceptions for. + * @param {Array} list An array of pairs, where the first element of each pair + * is the filter string, and the second is the setting (allow/block). + */ + ContentSettings.setExceptions = function(type, list) { + // Not implemented. + }; + + ContentSettings.setHandlers = function(list) { + // Not implemented. + }; + + ContentSettings.setIgnoredHandlers = function(list) { + // Not implemented. + }; + + ContentSettings.setOTRExceptions = function(type, list) { + // Not implemented. + }; + + /** + * Enables the Pepper Flash camera and microphone settings. + * Please note that whether the settings are actually showed or not is also + * affected by the style class pepper-flash-settings. + */ + ContentSettings.enablePepperFlashCameraMicSettings = function() { + // Not implemented. + } + + // Export + return { + ContentSettings: ContentSettings + }; + +}); + +} diff --git a/chromium/chrome/browser/resources/options/content_settings_exceptions_area.html b/chromium/chrome/browser/resources/options/content_settings_exceptions_area.html new file mode 100644 index 00000000000..1306379022b --- /dev/null +++ b/chromium/chrome/browser/resources/options/content_settings_exceptions_area.html @@ -0,0 +1,140 @@ +<div id="content-settings-exceptions-area" class="page" hidden> + <div class="close-button"></div> + <h1></h1> + <div class="content-area"> + <div id="exception-column-headers"> + <div id="exception-pattern-column" i18n-content="exceptionPatternHeader"> + </div> + <div id="exception-behavior-column" + i18n-content="exceptionBehaviorHeader"> + </div> + </div> + <div contentType="cookies"> + <list mode="normal"></list> + <div> + <span class="otr-explanation" i18n-content="otr_exceptions_explanation"> + </span> + <list mode="otr"></list> + </div> + <div class="flash-plugin-area"> + <a i18n-values="href:flash_storage_url" target="_blank" + i18n-content="flash_storage_settings"> + </a> + </div> + </div> + <div contentType="images"> + <list mode="normal"></list> + <div> + <span class="otr-explanation" i18n-content="otr_exceptions_explanation"> + </span> + <list mode="otr"></list> + </div> + </div> + <div contentType="javascript"> + <list mode="normal"></list> + <div> + <span class="otr-explanation" i18n-content="otr_exceptions_explanation"> + </span> + <list mode="otr"></list> + </div> + </div> + <div contentType="plugins"> + <list mode="normal"></list> + <div> + <span class="otr-explanation" i18n-content="otr_exceptions_explanation"> + </span> + <list mode="otr"></list> + </div> + </div> + <div contentType="popups"> + <list mode="normal"></list> + <div> + <span class="otr-explanation" i18n-content="otr_exceptions_explanation"> + </span> + <list mode="otr"></list> + </div> + </div> + <div contentType="location"> + <list mode="normal"></list> + </div> + <div contentType="notifications"> + <list mode="normal"></list> + </div> + <div contentType="fullscreen"> + <list mode="normal"></list> + <div> + <span class="otr-explanation" i18n-content="otr_exceptions_explanation"> + </span> + <list mode="otr"></list> + </div> + </div> + <div contentType="mouselock"> + <list mode="normal"></list> + <div> + <span class="otr-explanation" i18n-content="otr_exceptions_explanation"> + </span> + <list mode="otr"></list> + </div> + </div> + <div contentType="protectedContent"> + <list mode="normal"></list> + <div> + <span class="otr-explanation" i18n-content="otr_exceptions_explanation"> + </span> + <list mode="otr"></list> + </div> + </div> + <div id="media-column-header" class="media-header"> + <div id="exception-pattern-column"></div> + <div id="media-audio-column" i18n-content=mediaAudioExceptionHeader> + </div> + <div id="media-video-column" i18n-content="mediaVideoExceptionHeader"> + </div> + </div> + <div contentType="media-stream"> + <list mode="normal"></list> + <div> + <span class="otr-explanation" i18n-content="otr_exceptions_explanation"> + </span> + <list mode="otr"></list> + </div> + <div id="media-pepper-flash-exceptions" class="pepper-flash-settings"> + <span i18n-content="mediaPepperFlashExceptionsDivergedLabel"></span> + <a target="_blank" i18n-content="mediaPepperFlashChangeLink" + i18n-values="href:mediaPepperFlashWebsitePrivacyURL"></a> + </div> + </div> + <div contentType="ppapi-broker"> + <list mode="normal"></list> + <div> + <span class="otr-explanation" i18n-content="otr_exceptions_explanation"> + </span> + <list mode="otr"></list> + </div> + </div> + <div contentType="multiple-automatic-downloads"> + <list mode="normal"></list> + </div> + <div contentType="midi-sysex"> + <list mode="normal"></list> + <div> + <span class="otr-explanation" i18n-content="otr_exceptions_explanation"> + </span> + <list mode="otr"></list> + </div> + </div> + </div> + <div class="action-area"> + <div class="hbox stretch"> + <a target="_blank" i18n-content="learnMore" + i18n-values="href:exceptionsLearnMoreUrl"></a> + </div> + <div class="action-area-right"> + <div class="button-strip"> + <button id="content-settings-exceptions-overlay-confirm" + class="default-button" i18n-content="done"> + </button> + </div> + </div> + </div> +</div> diff --git a/chromium/chrome/browser/resources/options/content_settings_exceptions_area.js b/chromium/chrome/browser/resources/options/content_settings_exceptions_area.js new file mode 100644 index 00000000000..31c6a5ed07e --- /dev/null +++ b/chromium/chrome/browser/resources/options/content_settings_exceptions_area.js @@ -0,0 +1,670 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options.contentSettings', function() { + /** @const */ var ControlledSettingIndicator = + options.ControlledSettingIndicator; + /** @const */ var InlineEditableItemList = options.InlineEditableItemList; + /** @const */ var InlineEditableItem = options.InlineEditableItem; + /** @const */ var ArrayDataModel = cr.ui.ArrayDataModel; + + /** + * Creates a new exceptions list item. + * + * @param {string} contentType The type of the list. + * @param {string} mode The browser mode, 'otr' or 'normal'. + * @param {boolean} enableAskOption Whether to show an 'ask every time' + * option in the select. + * @param {Object} exception A dictionary that contains the data of the + * exception. + * @constructor + * @extends {options.InlineEditableItem} + */ + function ExceptionsListItem(contentType, mode, enableAskOption, exception) { + var el = cr.doc.createElement('div'); + el.mode = mode; + el.contentType = contentType; + el.enableAskOption = enableAskOption; + el.dataItem = exception; + el.__proto__ = ExceptionsListItem.prototype; + el.decorate(); + + return el; + } + + ExceptionsListItem.prototype = { + __proto__: InlineEditableItem.prototype, + + /** + * Called when an element is decorated as a list item. + */ + decorate: function() { + InlineEditableItem.prototype.decorate.call(this); + + this.isPlaceholder = !this.pattern; + var patternCell = this.createEditableTextCell(this.pattern); + patternCell.className = 'exception-pattern'; + patternCell.classList.add('weakrtl'); + this.contentElement.appendChild(patternCell); + if (this.pattern) + this.patternLabel = patternCell.querySelector('.static-text'); + var input = patternCell.querySelector('input'); + + // TODO(stuartmorgan): Create an createEditableSelectCell abstracting + // this code. + // Setting label for display mode. |pattern| will be null for the 'add new + // exception' row. + if (this.pattern) { + var settingLabel = cr.doc.createElement('span'); + settingLabel.textContent = this.settingForDisplay(); + settingLabel.className = 'exception-setting'; + settingLabel.setAttribute('displaymode', 'static'); + this.contentElement.appendChild(settingLabel); + this.settingLabel = settingLabel; + } + + // Setting select element for edit mode. + var select = cr.doc.createElement('select'); + var optionAllow = cr.doc.createElement('option'); + optionAllow.textContent = loadTimeData.getString('allowException'); + optionAllow.value = 'allow'; + select.appendChild(optionAllow); + + if (this.enableAskOption) { + var optionAsk = cr.doc.createElement('option'); + optionAsk.textContent = loadTimeData.getString('askException'); + optionAsk.value = 'ask'; + select.appendChild(optionAsk); + } + + if (this.contentType == 'cookies') { + var optionSession = cr.doc.createElement('option'); + optionSession.textContent = loadTimeData.getString('sessionException'); + optionSession.value = 'session'; + select.appendChild(optionSession); + } + + if (this.contentType != 'fullscreen') { + var optionBlock = cr.doc.createElement('option'); + optionBlock.textContent = loadTimeData.getString('blockException'); + optionBlock.value = 'block'; + select.appendChild(optionBlock); + } + + if (this.isEmbeddingRule()) { + this.patternLabel.classList.add('sublabel'); + this.editable = false; + } + + if (this.setting == 'default') { + // Items that don't have their own settings (parents of 'embedded on' + // items) aren't deletable. + this.deletable = false; + this.editable = false; + } + + this.addEditField(select, this.settingLabel); + this.contentElement.appendChild(select); + select.className = 'exception-setting'; + select.setAttribute('aria-labelledby', 'exception-behavior-column'); + + if (this.pattern) + select.setAttribute('displaymode', 'edit'); + + if (this.contentType == 'media-stream') { + this.settingLabel.classList.add('media-audio-setting'); + + var videoSettingLabel = cr.doc.createElement('span'); + videoSettingLabel.textContent = this.videoSettingForDisplay(); + videoSettingLabel.className = 'exception-setting'; + videoSettingLabel.classList.add('media-video-setting'); + videoSettingLabel.setAttribute('displaymode', 'static'); + this.contentElement.appendChild(videoSettingLabel); + } + + // Used to track whether the URL pattern in the input is valid. + // This will be true if the browser process has informed us that the + // current text in the input is valid. Changing the text resets this to + // false, and getting a response from the browser sets it back to true. + // It starts off as false for empty string (new exceptions) or true for + // already-existing exceptions (which we assume are valid). + this.inputValidityKnown = this.pattern; + // This one tracks the actual validity of the pattern in the input. This + // starts off as true so as not to annoy the user when he adds a new and + // empty input. + this.inputIsValid = true; + + this.input = input; + this.select = select; + + this.updateEditables(); + + // Editing notifications, geolocation and media-stream is disabled for + // now. + if (this.contentType == 'notifications' || + this.contentType == 'location' || + this.contentType == 'media-stream') { + this.editable = false; + } + + // If the source of the content setting exception is not a user + // preference, that source controls the exception and the user cannot edit + // or delete it. + var controlledBy = + this.dataItem.source && this.dataItem.source != 'preference' ? + this.dataItem.source : null; + + if (controlledBy) { + this.setAttribute('controlled-by', controlledBy); + this.deletable = false; + this.editable = false; + } + + if (controlledBy == 'policy' || controlledBy == 'extension') { + this.querySelector('.row-delete-button').hidden = true; + var indicator = ControlledSettingIndicator(); + indicator.setAttribute('content-exception', this.contentType); + // Create a synthetic pref change event decorated as + // CoreOptionsHandler::CreateValueForPref() does. + var event = new Event(this.contentType); + event.value = { controlledBy: controlledBy }; + indicator.handlePrefChange(event); + this.appendChild(indicator); + } + + // If the exception comes from a hosted app, display the name and the + // icon of the app. + if (controlledBy == 'HostedApp') { + this.title = + loadTimeData.getString('set_by') + ' ' + this.dataItem.appName; + var button = this.querySelector('.row-delete-button'); + // Use the host app's favicon (16px, match bigger size). + // See c/b/ui/webui/extensions/extension_icon_source.h + // for a description of the chrome://extension-icon URL. + button.style.backgroundImage = + 'url(\'chrome://extension-icon/' + this.dataItem.appId + '/16/1\')'; + } + + var listItem = this; + // Handle events on the editable nodes. + input.oninput = function(event) { + listItem.inputValidityKnown = false; + chrome.send('checkExceptionPatternValidity', + [listItem.contentType, listItem.mode, input.value]); + }; + + // Listen for edit events. + this.addEventListener('canceledit', this.onEditCancelled_); + this.addEventListener('commitedit', this.onEditCommitted_); + }, + + isEmbeddingRule: function() { + return this.dataItem.embeddingOrigin && + this.dataItem.embeddingOrigin !== this.dataItem.origin; + }, + + /** + * The pattern (e.g., a URL) for the exception. + * + * @type {string} + */ + get pattern() { + if (!this.isEmbeddingRule()) { + return this.dataItem.origin; + } else { + return loadTimeData.getStringF('embeddedOnHost', + this.dataItem.embeddingOrigin); + } + + return this.dataItem.displayPattern; + }, + set pattern(pattern) { + if (!this.editable) + console.error('Tried to change uneditable pattern'); + + this.dataItem.displayPattern = pattern; + }, + + /** + * The setting (allow/block) for the exception. + * + * @type {string} + */ + get setting() { + return this.dataItem.setting; + }, + set setting(setting) { + this.dataItem.setting = setting; + }, + + /** + * Gets a human-readable setting string. + * + * @return {string} The display string. + */ + settingForDisplay: function() { + return this.getDisplayStringForSetting(this.setting); + }, + + /** + * media video specific function. + * Gets a human-readable video setting string. + * + * @return {string} The display string. + */ + videoSettingForDisplay: function() { + return this.getDisplayStringForSetting(this.dataItem.video); + }, + + /** + * Gets a human-readable display string for setting. + * + * @param {string} setting The setting to be displayed. + * @return {string} The display string. + */ + getDisplayStringForSetting: function(setting) { + if (setting == 'allow') + return loadTimeData.getString('allowException'); + else if (setting == 'block') + return loadTimeData.getString('blockException'); + else if (setting == 'ask') + return loadTimeData.getString('askException'); + else if (setting == 'session') + return loadTimeData.getString('sessionException'); + else if (setting == 'default') + return ''; + + console.error('Unknown setting: [' + setting + ']'); + return ''; + }, + + /** + * Update this list item to reflect whether the input is a valid pattern. + * + * @param {boolean} valid Whether said pattern is valid in the context of a + * content exception setting. + */ + setPatternValid: function(valid) { + if (valid || !this.input.value) + this.input.setCustomValidity(''); + else + this.input.setCustomValidity(' '); + this.inputIsValid = valid; + this.inputValidityKnown = true; + }, + + /** + * Set the <input> to its original contents. Used when the user quits + * editing. + */ + resetInput: function() { + this.input.value = this.pattern; + }, + + /** + * Copy the data model values to the editable nodes. + */ + updateEditables: function() { + this.resetInput(); + + var settingOption = + this.select.querySelector('[value=\'' + this.setting + '\']'); + if (settingOption) + settingOption.selected = true; + }, + + /** @override */ + get currentInputIsValid() { + return this.inputValidityKnown && this.inputIsValid; + }, + + /** @override */ + get hasBeenEdited() { + var livePattern = this.input.value; + var liveSetting = this.select.value; + return livePattern != this.pattern || liveSetting != this.setting; + }, + + /** + * Called when committing an edit. + * + * @param {Event} e The end event. + * @private + */ + onEditCommitted_: function(e) { + var newPattern = this.input.value; + var newSetting = this.select.value; + + this.finishEdit(newPattern, newSetting); + }, + + /** + * Called when cancelling an edit; resets the control states. + * + * @param {Event} e The cancel event. + * @private + */ + onEditCancelled_: function() { + this.updateEditables(); + this.setPatternValid(true); + }, + + /** + * Editing is complete; update the model. + * + * @param {string} newPattern The pattern that the user entered. + * @param {string} newSetting The setting the user chose. + */ + finishEdit: function(newPattern, newSetting) { + this.patternLabel.textContent = newPattern; + this.settingLabel.textContent = this.settingForDisplay(); + var oldPattern = this.pattern; + this.pattern = newPattern; + this.setting = newSetting; + + // TODO(estade): this will need to be updated if geolocation/notifications + // become editable. + if (oldPattern != newPattern) { + chrome.send('removeException', + [this.contentType, this.mode, oldPattern]); + } + + chrome.send('setException', + [this.contentType, this.mode, newPattern, newSetting]); + }, + }; + + /** + * Creates a new list item for the Add New Item row, which doesn't represent + * an actual entry in the exceptions list but allows the user to add new + * exceptions. + * + * @param {string} contentType The type of the list. + * @param {string} mode The browser mode, 'otr' or 'normal'. + * @param {boolean} enableAskOption Whether to show an 'ask every time' option + * in the select. + * @constructor + * @extends {cr.ui.ExceptionsListItem} + */ + function ExceptionsAddRowListItem(contentType, mode, enableAskOption) { + var el = cr.doc.createElement('div'); + el.mode = mode; + el.contentType = contentType; + el.enableAskOption = enableAskOption; + el.dataItem = []; + el.__proto__ = ExceptionsAddRowListItem.prototype; + el.decorate(); + + return el; + } + + ExceptionsAddRowListItem.prototype = { + __proto__: ExceptionsListItem.prototype, + + decorate: function() { + ExceptionsListItem.prototype.decorate.call(this); + + this.input.placeholder = + loadTimeData.getString('addNewExceptionInstructions'); + + // Do we always want a default of allow? + this.setting = 'allow'; + }, + + /** + * Clear the <input> and let the placeholder text show again. + */ + resetInput: function() { + this.input.value = ''; + }, + + /** @override */ + get hasBeenEdited() { + return this.input.value != ''; + }, + + /** + * Editing is complete; update the model. As long as the pattern isn't + * empty, we'll just add it. + * + * @param {string} newPattern The pattern that the user entered. + * @param {string} newSetting The setting the user chose. + */ + finishEdit: function(newPattern, newSetting) { + this.resetInput(); + chrome.send('setException', + [this.contentType, this.mode, newPattern, newSetting]); + }, + }; + + /** + * Creates a new exceptions list. + * + * @constructor + * @extends {cr.ui.List} + */ + var ExceptionsList = cr.ui.define('list'); + + ExceptionsList.prototype = { + __proto__: InlineEditableItemList.prototype, + + /** + * Called when an element is decorated as a list. + */ + decorate: function() { + InlineEditableItemList.prototype.decorate.call(this); + + this.classList.add('settings-list'); + + for (var parentNode = this.parentNode; parentNode; + parentNode = parentNode.parentNode) { + if (parentNode.hasAttribute('contentType')) { + this.contentType = parentNode.getAttribute('contentType'); + break; + } + } + + this.mode = this.getAttribute('mode'); + + // Whether the exceptions in this list allow an 'Ask every time' option. + this.enableAskOption = this.contentType == 'plugins'; + + this.autoExpands = true; + this.reset(); + }, + + /** + * Creates an item to go in the list. + * + * @param {Object} entry The element from the data model for this row. + */ + createItem: function(entry) { + if (entry) { + return new ExceptionsListItem(this.contentType, + this.mode, + this.enableAskOption, + entry); + } else { + var addRowItem = new ExceptionsAddRowListItem(this.contentType, + this.mode, + this.enableAskOption); + addRowItem.deletable = false; + return addRowItem; + } + }, + + /** + * Sets the exceptions in the js model. + * + * @param {Object} entries A list of dictionaries of values, each dictionary + * represents an exception. + */ + setExceptions: function(entries) { + var deleteCount = this.dataModel.length; + + if (this.isEditable()) { + // We don't want to remove the Add New Exception row. + deleteCount = deleteCount - 1; + } + + var args = [0, deleteCount]; + args.push.apply(args, entries); + this.dataModel.splice.apply(this.dataModel, args); + }, + + /** + * The browser has finished checking a pattern for validity. Update the list + * item to reflect this. + * + * @param {string} pattern The pattern. + * @param {bool} valid Whether said pattern is valid in the context of a + * content exception setting. + */ + patternValidityCheckComplete: function(pattern, valid) { + var listItems = this.items; + for (var i = 0; i < listItems.length; i++) { + var listItem = listItems[i]; + // Don't do anything for messages for the item if it is not the intended + // recipient, or if the response is stale (i.e. the input value has + // changed since we sent the request to analyze it). + if (pattern == listItem.input.value) + listItem.setPatternValid(valid); + } + }, + + /** + * Returns whether the rows are editable in this list. + */ + isEditable: function() { + // Exceptions of the following lists are not editable for now. + return !(this.contentType == 'notifications' || + this.contentType == 'location' || + this.contentType == 'fullscreen' || + this.contentType == 'media-stream'); + }, + + /** + * Removes all exceptions from the js model. + */ + reset: function() { + if (this.isEditable()) { + // The null creates the Add New Exception row. + this.dataModel = new ArrayDataModel([null]); + } else { + this.dataModel = new ArrayDataModel([]); + } + }, + + /** @override */ + deleteItemAtIndex: function(index) { + var listItem = this.getListItemByIndex(index); + if (!listItem.deletable) + return; + + var dataItem = listItem.dataItem; + var args = [listItem.contentType]; + if (listItem.contentType == 'notifications') + args.push(dataItem.origin, dataItem.setting); + else + args.push(listItem.mode, dataItem.origin, dataItem.embeddingOrigin); + + chrome.send('removeException', args); + }, + }; + + var OptionsPage = options.OptionsPage; + + /** + * Encapsulated handling of content settings list subpage. + * + * @constructor + */ + function ContentSettingsExceptionsArea() { + OptionsPage.call(this, 'contentExceptions', + loadTimeData.getString('contentSettingsPageTabTitle'), + 'content-settings-exceptions-area'); + } + + cr.addSingletonGetter(ContentSettingsExceptionsArea); + + ContentSettingsExceptionsArea.prototype = { + __proto__: OptionsPage.prototype, + + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + + var exceptionsLists = this.pageDiv.querySelectorAll('list'); + for (var i = 0; i < exceptionsLists.length; i++) { + options.contentSettings.ExceptionsList.decorate(exceptionsLists[i]); + } + + ContentSettingsExceptionsArea.hideOTRLists(false); + + // If the user types in the URL without a hash, show just cookies. + this.showList('cookies'); + + $('content-settings-exceptions-overlay-confirm').onclick = + OptionsPage.closeOverlay.bind(OptionsPage); + }, + + /** + * Shows one list and hides all others. + * + * @param {string} type The content type. + */ + showList: function(type) { + var header = this.pageDiv.querySelector('h1'); + header.textContent = loadTimeData.getString(type + '_header'); + + var divs = this.pageDiv.querySelectorAll('div[contentType]'); + for (var i = 0; i < divs.length; i++) { + if (divs[i].getAttribute('contentType') == type) + divs[i].hidden = false; + else + divs[i].hidden = true; + } + + var mediaHeader = this.pageDiv.querySelector('.media-header'); + mediaHeader.hidden = type != 'media-stream'; + }, + + /** + * Called after the page has been shown. Show the content type for the + * location's hash. + */ + didShowPage: function() { + var hash = location.hash; + if (hash) + this.showList(hash.slice(1)); + }, + }; + + /** + * Called when the last incognito window is closed. + */ + ContentSettingsExceptionsArea.OTRProfileDestroyed = function() { + this.hideOTRLists(true); + }; + + /** + * Hides the incognito exceptions lists and optionally clears them as well. + * @param {boolean} clear Whether to clear the lists. + */ + ContentSettingsExceptionsArea.hideOTRLists = function(clear) { + var otrLists = document.querySelectorAll('list[mode=otr]'); + + for (var i = 0; i < otrLists.length; i++) { + otrLists[i].parentNode.hidden = true; + if (clear) + otrLists[i].reset(); + } + }; + + return { + ExceptionsListItem: ExceptionsListItem, + ExceptionsAddRowListItem: ExceptionsAddRowListItem, + ExceptionsList: ExceptionsList, + ContentSettingsExceptionsArea: ContentSettingsExceptionsArea, + }; +}); diff --git a/chromium/chrome/browser/resources/options/content_settings_ui.js b/chromium/chrome/browser/resources/options/content_settings_ui.js new file mode 100644 index 00000000000..dc7b0c496a2 --- /dev/null +++ b/chromium/chrome/browser/resources/options/content_settings_ui.js @@ -0,0 +1,67 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + + ////////////////////////////////////////////////////////////////////////////// + // ContentSettingsRadio class: + + // Define a constructor that uses an input element as its underlying element. + var ContentSettingsRadio = cr.ui.define('input'); + + ContentSettingsRadio.prototype = { + __proto__: HTMLInputElement.prototype, + + /** + * Initialization function for the cr.ui framework. + */ + decorate: function() { + this.type = 'radio'; + var self = this; + + this.addEventListener('change', + function(e) { + chrome.send('setContentFilter', [this.name, this.value]); + }); + }, + }; + + /** + * Whether the content setting is controlled by something else than the user's + * settings (either 'policy' or 'extension'). + * @type {string} + */ + cr.defineProperty(ContentSettingsRadio, 'controlledBy', cr.PropertyKind.ATTR); + + ////////////////////////////////////////////////////////////////////////////// + // HandlersEnabledRadio class: + + // Define a constructor that uses an input element as its underlying element. + var HandlersEnabledRadio = cr.ui.define('input'); + + HandlersEnabledRadio.prototype = { + __proto__: HTMLInputElement.prototype, + + /** + * Initialization function for the cr.ui framework. + */ + decorate: function() { + this.type = 'radio'; + var self = this; + + this.addEventListener('change', + function(e) { + chrome.send('setHandlersEnabled', [this.value == 'allow']); + }); + }, + }; + + // Export + return { + ContentSettingsRadio: ContentSettingsRadio, + HandlersEnabledRadio: HandlersEnabledRadio + }; + +}); + diff --git a/chromium/chrome/browser/resources/options/controlled_setting.css b/chromium/chrome/browser/resources/options/controlled_setting.css new file mode 100644 index 00000000000..089e27dbbd7 --- /dev/null +++ b/chromium/chrome/browser/resources/options/controlled_setting.css @@ -0,0 +1,127 @@ +/* Copyright 2013 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ + +/* Controlled setting indicator and bubble. */ + +.controlled-setting-with-label { + -webkit-box-align: center; + display: -webkit-box; + padding-bottom: 7px; + padding-top: 7px; +} + +.controlled-setting-with-label > input + span { + -webkit-box-align: center; + -webkit-box-flex: 1; + -webkit-margin-start: 0.6em; + display: -webkit-box; +} + +.controlled-setting-with-label > input:disabled + span label { + color: #999; +} + +.controlled-setting-with-label label { + display: inline; + padding: 0; +} + +input:-webkit-any([type='text'],[type='url'],:not([type])) + + .controlled-setting-indicator { + -webkit-margin-start: 5px; +} + +.controlled-setting-indicator:not([controlled-by]) { + display: none; +} + +.controlled-setting-indicator[controlled-by='policy'] > div { + background-image: url('chrome://theme/IDR_CONTROLLED_SETTING_MANDATORY'); +} + +.controlled-setting-indicator[controlled-by='owner'] > div { + background-image: url('chrome://theme/IDR_CONTROLLED_SETTING_MANDATORY'); +} + +.controlled-setting-indicator[controlled-by='extension'] > div { + background-image: url('chrome://theme/IDR_CONTROLLED_SETTING_EXTENSION'); +} + +.controlled-setting-indicator:-webkit-any([controlled-by='recommended'], + [controlled-by='hasRecommendation']) > div { + background-image: url('chrome://theme/IDR_CONTROLLED_SETTING_MANDATORY'); +} + +.controlled-setting-bubble-content { + -webkit-padding-start: 30px; + background-repeat: no-repeat; + background-size: 22px; + min-height: 32px; +} + +.controlled-setting-bubble-content[controlled-by='policy'] { + background-image: url('chrome://theme/IDR_CONTROLLED_SETTING_MANDATORY'); +} + +.controlled-setting-bubble-content[controlled-by='owner'] { + background-image: url('chrome://theme/IDR_CONTROLLED_SETTING_MANDATORY'); +} + +.controlled-setting-bubble-content[controlled-by='extension'] { + background-image: url('chrome://theme/IDR_CONTROLLED_SETTING_EXTENSION'); +} + +.controlled-setting-bubble-content:-webkit-any([controlled-by='recommended'], + [controlled-by='hasRecommendation']) { + background-image: url('chrome://theme/IDR_CONTROLLED_SETTING_MANDATORY'); +} + +html[dir='rtl'] .controlled-setting-bubble-content { + background-position: right top; +} + +.controlled-setting-bubble-action { + padding: 0 !important; +} + +.controlled-setting-bubble-content-row { + height: 35px; + position: relative; +} + +.controlled-setting-bubble-extension-name { + -webkit-padding-start: 30px; + background-repeat: no-repeat; + font-weight: bold; + height: 24px; + margin-top: -12px; + overflow: hidden; + padding-top: 3px; + position: absolute; + text-overflow: ellipsis; + top: 50%; + white-space: nowrap; + width: 215px; +} + +html[dir='rtl'] .controlled-setting-bubble-extension-name { + background-position: right top; +} + +.controlled-setting-bubble-extension-manage-link { + margin-top: -0.5em; + position: absolute; + top: 50%; +} + +.controlled-setting-bubble-extension-disable-button { + bottom: 0; + position: absolute; + right: 0; +} + +html[dir='rtl'] .controlled-setting-bubble-extension-disable-button { + left: 0; + right: auto; +} diff --git a/chromium/chrome/browser/resources/options/controlled_setting.js b/chromium/chrome/browser/resources/options/controlled_setting.js new file mode 100644 index 00000000000..28674bd888b --- /dev/null +++ b/chromium/chrome/browser/resources/options/controlled_setting.js @@ -0,0 +1,236 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + var Preferences = options.Preferences; + + /** + * A controlled setting indicator that can be placed on a setting as an + * indicator that the value is controlled by some external entity such as + * policy or an extension. + * @constructor + * @extends {HTMLSpanElement} + */ + var ControlledSettingIndicator = cr.ui.define('span'); + + ControlledSettingIndicator.prototype = { + __proto__: cr.ui.BubbleButton.prototype, + + /** + * Decorates the base element to show the proper icon. + */ + decorate: function() { + cr.ui.BubbleButton.prototype.decorate.call(this); + this.classList.add('controlled-setting-indicator'); + + // If there is a pref, track its controlledBy and recommendedValue + // properties in order to be able to bring up the correct bubble. + if (this.pref) { + Preferences.getInstance().addEventListener( + this.pref, this.handlePrefChange.bind(this)); + this.resetHandler = this.clearAssociatedPref_; + } + }, + + /** + * The given handler will be called when the user clicks on the 'reset to + * recommended value' link shown in the indicator bubble. The |this| object + * will be the indicator itself. + * @param {function()} handler The handler to be called. + */ + set resetHandler(handler) { + this.resetHandler_ = handler; + }, + + /** + * Clears the preference associated with this indicator. + * @private + */ + clearAssociatedPref_: function() { + Preferences.clearPref(this.pref, !this.dialogPref); + }, + + /* Handle changes to the associated pref by hiding any currently visible + * bubble and updating the controlledBy property. + * @param {Event} event Pref change event. + */ + handlePrefChange: function(event) { + OptionsPage.hideBubble(); + if (event.value.controlledBy) { + if (!this.value || String(event.value.value) == this.value) { + this.controlledBy = event.value.controlledBy; + if (event.value.extension) { + if (this.pref == 'session.restore_on_startup' || + this.pref == 'homepage_is_newtabpage') { + // Special case for the restore on startup, which is implied + // by the startup pages settings being controlled by an + // extension, and same for the home page as NTP, so we don't want + // to show two buttons in these cases. + // TODO(mad): Find a better way to handle this. + this.controlledBy = null; + } else { + this.extensionId = event.value.extension.id; + this.extensionIcon = event.value.extension.icon; + this.extensionName = event.value.extension.name; + } + } + } else { + this.controlledBy = null; + } + } else if (event.value.recommendedValue != undefined) { + this.controlledBy = + !this.value || String(event.value.recommendedValue) == this.value ? + 'hasRecommendation' : null; + } else { + this.controlledBy = null; + } + }, + + /** + * Open or close a bubble with further information about the pref. + * @private + */ + toggleBubble_: function() { + if (this.showingBubble) { + OptionsPage.hideBubble(); + } else { + var self = this; + + // Construct the bubble text. + if (this.hasAttribute('plural')) { + var defaultStrings = { + 'policy': loadTimeData.getString('controlledSettingsPolicy'), + 'extension': loadTimeData.getString('controlledSettingsExtension'), + 'extensionWithName': loadTimeData.getString( + 'controlledSettingsExtensionWithName'), + }; + } else { + var defaultStrings = { + 'policy': loadTimeData.getString('controlledSettingPolicy'), + 'extension': loadTimeData.getString('controlledSettingExtension'), + 'extensionWithName': loadTimeData.getString( + 'controlledSettingExtensionWithName'), + 'recommended': + loadTimeData.getString('controlledSettingRecommended'), + 'hasRecommendation': + loadTimeData.getString('controlledSettingHasRecommendation'), + }; + if (cr.isChromeOS) { + defaultStrings.owner = + loadTimeData.getString('controlledSettingOwner'); + } + } + + // No controller, no bubble. + if (!this.controlledBy || !(this.controlledBy in defaultStrings)) + return; + + var text = defaultStrings[this.controlledBy]; + if (this.controlledBy == 'extension' && this.extensionName) + text = defaultStrings.extensionWithName; + + // Apply text overrides. + if (this.hasAttribute('text' + this.controlledBy)) + text = this.getAttribute('text' + this.controlledBy); + + // Create the DOM tree. + var content = document.createElement('div'); + content.className = 'controlled-setting-bubble-content'; + content.setAttribute('controlled-by', this.controlledBy); + content.textContent = text; + + if (this.controlledBy == 'hasRecommendation' && this.resetHandler_ && + !this.readOnly) { + var container = document.createElement('div'); + var action = document.createElement('button'); + action.classList.add('link-button'); + action.classList.add('controlled-setting-bubble-action'); + action.textContent = + loadTimeData.getString('controlledSettingFollowRecommendation'); + action.addEventListener('click', function(event) { + self.resetHandler_(); + }); + container.appendChild(action); + content.appendChild(container); + } else if (this.controlledBy == 'extension' && this.extensionName) { + var extensionContainer = + $('extension-controlled-settings-bubble-template'). + cloneNode(true); + // No need for an id anymore, and thus remove to avoid id collision. + extensionContainer.removeAttribute('id'); + extensionContainer.hidden = false; + + var extensionName = extensionContainer.querySelector( + '.controlled-setting-bubble-extension-name'); + extensionName.textContent = this.extensionName; + extensionName.style.backgroundImage = + 'url("' + this.extensionIcon + '")'; + + var manageLink = extensionContainer.querySelector( + '.controlled-setting-bubble-extension-manage-link'); + manageLink.onclick = function() { + uber.invokeMethodOnWindow( + window.top, 'showPage', {pageId: 'extensions'}); + }; + + var disableButton = extensionContainer.querySelector('button'); + var extensionId = this.extensionId; + disableButton.onclick = function() { + chrome.send('disableExtension', [extensionId]); + }; + content.appendChild(extensionContainer); + } + + OptionsPage.showBubble(content, this.image, this, this.location); + } + }, + }; + + /** + * The name of the associated preference. + * @type {string} + */ + cr.defineProperty(ControlledSettingIndicator, 'pref', cr.PropertyKind.ATTR); + + /** + * Whether this indicator is part of a dialog. If so, changes made to the + * associated preference take effect in the settings UI immediately but are + * only actually committed when the user confirms the dialog. If the user + * cancels the dialog instead, the changes are rolled back in the settings UI + * and never committed. + * @type {boolean} + */ + cr.defineProperty(ControlledSettingIndicator, 'dialogPref', + cr.PropertyKind.BOOL_ATTR); + + /** + * The value of the associated preference that the indicator represents. If + * this is not set, the indicator will be visible whenever any value is + * enforced or recommended. If it is set, the indicator will be visible only + * when the enforced or recommended value matches the value it represents. + * This allows multiple indicators to be created for a set of radio buttons, + * ensuring that only one of them is visible at a time. + */ + cr.defineProperty(ControlledSettingIndicator, 'value', + cr.PropertyKind.ATTR); + + /** + * The status of the associated preference: + * - 'policy': A specific value is enfoced by policy. + * - 'extension': A specific value is enforced by an extension. + * - 'recommended': A value is recommended by policy. The user could + * override this recommendation but has not done so. + * - 'hasRecommendation': A value is recommended by policy. The user has + * overridden this recommendation. + * - unset: The value is controlled by the user alone. + * @type {string} + */ + cr.defineProperty(ControlledSettingIndicator, 'controlledBy', + cr.PropertyKind.ATTR); + + // Export. + return { + ControlledSettingIndicator: ControlledSettingIndicator + }; +}); diff --git a/chromium/chrome/browser/resources/options/cookies_list.js b/chromium/chrome/browser/resources/options/cookies_list.js new file mode 100644 index 00000000000..c20ac63c158 --- /dev/null +++ b/chromium/chrome/browser/resources/options/cookies_list.js @@ -0,0 +1,921 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + /** @const */ var DeletableItemList = options.DeletableItemList; + /** @const */ var DeletableItem = options.DeletableItem; + /** @const */ var ArrayDataModel = cr.ui.ArrayDataModel; + /** @const */ var ListSingleSelectionModel = cr.ui.ListSingleSelectionModel; + + // This structure maps the various cookie type names from C++ (hence the + // underscores) to arrays of the different types of data each has, along with + // the i18n name for the description of that data type. + /** @const */ var cookieInfo = { + 'cookie': [['name', 'label_cookie_name'], + ['content', 'label_cookie_content'], + ['domain', 'label_cookie_domain'], + ['path', 'label_cookie_path'], + ['sendfor', 'label_cookie_send_for'], + ['accessibleToScript', 'label_cookie_accessible_to_script'], + ['created', 'label_cookie_created'], + ['expires', 'label_cookie_expires']], + 'app_cache': [['manifest', 'label_app_cache_manifest'], + ['size', 'label_local_storage_size'], + ['created', 'label_cookie_created'], + ['accessed', 'label_cookie_last_accessed']], + 'database': [['name', 'label_cookie_name'], + ['desc', 'label_webdb_desc'], + ['size', 'label_local_storage_size'], + ['modified', 'label_local_storage_last_modified']], + 'local_storage': [['origin', 'label_local_storage_origin'], + ['size', 'label_local_storage_size'], + ['modified', 'label_local_storage_last_modified']], + 'indexed_db': [['origin', 'label_indexed_db_origin'], + ['size', 'label_indexed_db_size'], + ['modified', 'label_indexed_db_last_modified']], + 'file_system': [['origin', 'label_file_system_origin'], + ['persistent', 'label_file_system_persistent_usage'], + ['temporary', 'label_file_system_temporary_usage']], + 'server_bound_cert': [['serverId', 'label_server_bound_cert_server_id'], + ['certType', 'label_server_bound_cert_type'], + ['created', 'label_server_bound_cert_created']], + 'flash_lso': [['domain', 'label_cookie_domain']], + }; + + /** + * Returns the item's height, like offsetHeight but such that it works better + * when the page is zoomed. See the similar calculation in @{code cr.ui.List}. + * This version also accounts for the animation done in this file. + * @param {Element} item The item to get the height of. + * @return {number} The height of the item, calculated with zooming in mind. + */ + function getItemHeight(item) { + var height = item.style.height; + // Use the fixed animation target height if set, in case the element is + // currently being animated and we'd get an intermediate height below. + if (height && height.substr(-2) == 'px') + return parseInt(height.substr(0, height.length - 2)); + return item.getBoundingClientRect().height; + } + + /** + * Create tree nodes for the objects in the data array, and insert them all + * into the given list using its @{code splice} method at the given index. + * @param {Array.<Object>} data The data objects for the nodes to add. + * @param {number} start The index at which to start inserting the nodes. + * @return {Array.<CookieTreeNode>} An array of CookieTreeNodes added. + */ + function spliceTreeNodes(data, start, list) { + var nodes = data.map(function(x) { return new CookieTreeNode(x); }); + // Insert [start, 0] at the beginning of the array of nodes, making it + // into the arguments we want to pass to @{code list.splice} below. + nodes.splice(0, 0, start, 0); + list.splice.apply(list, nodes); + // Remove the [start, 0] prefix and return the array of nodes. + nodes.splice(0, 2); + return nodes; + } + + /** + * Adds information about an app that protects this data item to the + * @{code element}. + * @param {Element} element The DOM element the information should be + appended to. + * @param {{id: string, name: string}} appInfo Information about an app. + */ + function addAppInfo(element, appInfo) { + var img = element.ownerDocument.createElement('img'); + img.src = 'chrome://extension-icon/' + appInfo.id + '/16/1'; + element.title = loadTimeData.getString('label_protected_by_apps') + + ' ' + appInfo.name; + img.className = 'protecting-app'; + element.appendChild(img); + } + + var parentLookup = {}; + var lookupRequests = {}; + + /** + * Creates a new list item for sites data. Note that these are created and + * destroyed lazily as they scroll into and out of view, so they must be + * stateless. We cache the expanded item in @{code CookiesList} though, so it + * can keep state. (Mostly just which item is selected.) + * @param {Object} origin Data used to create a cookie list item. + * @param {CookiesList} list The list that will contain this item. + * @constructor + * @extends {DeletableItem} + */ + function CookieListItem(origin, list) { + var listItem = new DeletableItem(null); + listItem.__proto__ = CookieListItem.prototype; + + listItem.origin = origin; + listItem.list = list; + listItem.decorate(); + + // This hooks up updateOrigin() to the list item, makes the top-level + // tree nodes (i.e., origins) register their IDs in parentLookup, and + // causes them to request their children if they have none. Note that we + // have special logic in the setter for the parent property to make sure + // that we can still garbage collect list items when they scroll out of + // view, even though it appears that we keep a direct reference. + if (origin) { + origin.parent = listItem; + origin.updateOrigin(); + } + + return listItem; + } + + CookieListItem.prototype = { + __proto__: DeletableItem.prototype, + + /** @override */ + decorate: function() { + this.siteChild = this.ownerDocument.createElement('div'); + this.siteChild.className = 'cookie-site'; + this.dataChild = this.ownerDocument.createElement('div'); + this.dataChild.className = 'cookie-data'; + this.sizeChild = this.ownerDocument.createElement('div'); + this.sizeChild.className = 'cookie-size'; + this.itemsChild = this.ownerDocument.createElement('div'); + this.itemsChild.className = 'cookie-items'; + this.infoChild = this.ownerDocument.createElement('div'); + this.infoChild.className = 'cookie-details'; + this.infoChild.hidden = true; + + var remove = this.ownerDocument.createElement('button'); + remove.textContent = loadTimeData.getString('remove_cookie'); + remove.onclick = this.removeCookie_.bind(this); + this.infoChild.appendChild(remove); + var content = this.contentElement; + content.appendChild(this.siteChild); + content.appendChild(this.dataChild); + content.appendChild(this.sizeChild); + content.appendChild(this.itemsChild); + this.itemsChild.appendChild(this.infoChild); + if (this.origin && this.origin.data) { + this.siteChild.textContent = this.origin.data.title; + this.siteChild.setAttribute('title', this.origin.data.title); + } + this.itemList_ = []; + }, + + /** @type {boolean} */ + get expanded() { + return this.expanded_; + }, + set expanded(expanded) { + if (this.expanded_ == expanded) + return; + this.expanded_ = expanded; + if (expanded) { + var oldExpanded = this.list.expandedItem; + this.list.expandedItem = this; + this.updateItems_(); + if (oldExpanded) + oldExpanded.expanded = false; + this.classList.add('show-items'); + } else { + if (this.list.expandedItem == this) { + this.list.expandedItem = null; + } + this.style.height = ''; + this.itemsChild.style.height = ''; + this.classList.remove('show-items'); + } + }, + + /** + * The callback for the "remove" button shown when an item is selected. + * Requests that the currently selected cookie be removed. + * @private + */ + removeCookie_: function() { + if (this.selectedIndex_ >= 0) { + var item = this.itemList_[this.selectedIndex_]; + if (item && item.node) + chrome.send('removeCookie', [item.node.pathId]); + } + }, + + /** + * Disable animation within this cookie list item, in preparation for making + * changes that will need to be animated. Makes it possible to measure the + * contents without displaying them, to set animation targets. + * @private + */ + disableAnimation_: function() { + this.itemsHeight_ = getItemHeight(this.itemsChild); + this.classList.add('measure-items'); + }, + + /** + * Enable animation after changing the contents of this cookie list item. + * See @{code disableAnimation_}. + * @private + */ + enableAnimation_: function() { + if (!this.classList.contains('measure-items')) + this.disableAnimation_(); + this.itemsChild.style.height = ''; + // This will force relayout in order to calculate the new heights. + var itemsHeight = getItemHeight(this.itemsChild); + var fixedHeight = getItemHeight(this) + itemsHeight - this.itemsHeight_; + this.itemsChild.style.height = this.itemsHeight_ + 'px'; + // Force relayout before enabling animation, so that if we have + // changed things since the last layout, they will not be animated + // during subsequent layouts. + this.itemsChild.offsetHeight; + this.classList.remove('measure-items'); + this.itemsChild.style.height = itemsHeight + 'px'; + this.style.height = fixedHeight + 'px'; + }, + + /** + * Updates the origin summary to reflect changes in its items. + * Both CookieListItem and CookieTreeNode implement this API. + * This implementation scans the descendants to update the text. + */ + updateOrigin: function() { + var info = { + cookies: 0, + database: false, + localStorage: false, + appCache: false, + indexedDb: false, + fileSystem: false, + serverBoundCerts: 0, + }; + if (this.origin) + this.origin.collectSummaryInfo(info); + + var list = []; + if (info.cookies > 1) + list.push(loadTimeData.getStringF('cookie_plural', info.cookies)); + else if (info.cookies > 0) + list.push(loadTimeData.getString('cookie_singular')); + if (info.database || info.indexedDb) + list.push(loadTimeData.getString('cookie_database_storage')); + if (info.localStorage) + list.push(loadTimeData.getString('cookie_local_storage')); + if (info.appCache) + list.push(loadTimeData.getString('cookie_app_cache')); + if (info.fileSystem) + list.push(loadTimeData.getString('cookie_file_system')); + if (info.serverBoundCerts) + list.push(loadTimeData.getString('cookie_server_bound_cert')); + if (info.flashLSO) + list.push(loadTimeData.getString('cookie_flash_lso')); + + var text = ''; + for (var i = 0; i < list.length; ++i) { + if (text.length > 0) + text += ', ' + list[i]; + else + text = list[i]; + } + this.dataChild.textContent = text; + + var apps = info.appsProtectingThis; + for (var key in apps) { + addAppInfo(this.dataChild, apps[key]); + } + + if (info.quota && info.quota.totalUsage) + this.sizeChild.textContent = info.quota.totalUsage; + + if (this.expanded) + this.updateItems_(); + }, + + /** + * Updates the items section to reflect changes, animating to the new state. + * Removes existing contents and calls @{code CookieTreeNode.createItems}. + * @private + */ + updateItems_: function() { + this.disableAnimation_(); + this.itemsChild.textContent = ''; + this.infoChild.hidden = true; + this.selectedIndex_ = -1; + this.itemList_ = []; + if (this.origin) + this.origin.createItems(this); + this.itemsChild.appendChild(this.infoChild); + this.enableAnimation_(); + }, + + /** + * Append a new cookie node "bubble" to this list item. + * @param {CookieTreeNode} node The cookie node to add a bubble for. + * @param {Element} div The DOM element for the bubble itself. + * @return {number} The index the bubble was added at. + */ + appendItem: function(node, div) { + this.itemList_.push({node: node, div: div}); + this.itemsChild.appendChild(div); + return this.itemList_.length - 1; + }, + + /** + * The currently selected cookie node ("cookie bubble") index. + * @type {number} + * @private + */ + selectedIndex_: -1, + + /** + * Get the currently selected cookie node ("cookie bubble") index. + * @type {number} + */ + get selectedIndex() { + return this.selectedIndex_; + }, + + /** + * Set the currently selected cookie node ("cookie bubble") index to + * @{code itemIndex}, unselecting any previously selected node first. + * @param {number} itemIndex The index to set as the selected index. + */ + set selectedIndex(itemIndex) { + // Get the list index up front before we change anything. + var index = this.list.getIndexOfListItem(this); + // Unselect any previously selected item. + if (this.selectedIndex_ >= 0) { + var item = this.itemList_[this.selectedIndex_]; + if (item && item.div) + item.div.removeAttribute('selected'); + } + // Special case: decrementing -1 wraps around to the end of the list. + if (itemIndex == -2) + itemIndex = this.itemList_.length - 1; + // Check if we're going out of bounds and hide the item details. + if (itemIndex < 0 || itemIndex >= this.itemList_.length) { + this.selectedIndex_ = -1; + this.disableAnimation_(); + this.infoChild.hidden = true; + this.enableAnimation_(); + return; + } + // Set the new selected item and show the item details for it. + this.selectedIndex_ = itemIndex; + this.itemList_[itemIndex].div.setAttribute('selected', ''); + this.disableAnimation_(); + this.itemList_[itemIndex].node.setDetailText(this.infoChild, + this.list.infoNodes); + this.infoChild.hidden = false; + this.enableAnimation_(); + // If we're near the bottom of the list this may cause the list item to go + // beyond the end of the visible area. Fix it after the animation is done. + var list = this.list; + window.setTimeout(function() { list.scrollIndexIntoView(index); }, 150); + }, + }; + + /** + * {@code CookieTreeNode}s mirror the structure of the cookie tree lazily, and + * contain all the actual data used to generate the {@code CookieListItem}s. + * @param {Object} data The data object for this node. + * @constructor + */ + function CookieTreeNode(data) { + this.data = data; + this.children = []; + } + + CookieTreeNode.prototype = { + /** + * Insert the given list of cookie tree nodes at the given index. + * Both CookiesList and CookieTreeNode implement this API. + * @param {Array.<Object>} data The data objects for the nodes to add. + * @param {number} start The index at which to start inserting the nodes. + */ + insertAt: function(data, start) { + var nodes = spliceTreeNodes(data, start, this.children); + for (var i = 0; i < nodes.length; i++) + nodes[i].parent = this; + this.updateOrigin(); + }, + + /** + * Remove a cookie tree node from the given index. + * Both CookiesList and CookieTreeNode implement this API. + * @param {number} index The index of the tree node to remove. + */ + remove: function(index) { + if (index < this.children.length) { + this.children.splice(index, 1); + this.updateOrigin(); + } + }, + + /** + * Clears all children. + * Both CookiesList and CookieTreeNode implement this API. + * It is used by CookiesList.loadChildren(). + */ + clear: function() { + // We might leave some garbage in parentLookup for removed children. + // But that should be OK because parentLookup is cleared when we + // reload the tree. + this.children = []; + this.updateOrigin(); + }, + + /** + * The counter used by startBatchUpdates() and endBatchUpdates(). + * @type {number} + */ + batchCount_: 0, + + /** + * See cr.ui.List.startBatchUpdates(). + * Both CookiesList (via List) and CookieTreeNode implement this API. + */ + startBatchUpdates: function() { + this.batchCount_++; + }, + + /** + * See cr.ui.List.endBatchUpdates(). + * Both CookiesList (via List) and CookieTreeNode implement this API. + */ + endBatchUpdates: function() { + if (!--this.batchCount_) + this.updateOrigin(); + }, + + /** + * Requests updating the origin summary to reflect changes in this item. + * Both CookieListItem and CookieTreeNode implement this API. + */ + updateOrigin: function() { + if (!this.batchCount_ && this.parent) + this.parent.updateOrigin(); + }, + + /** + * Summarize the information in this node and update @{code info}. + * This will recurse into child nodes to summarize all descendants. + * @param {Object} info The info object from @{code updateOrigin}. + */ + collectSummaryInfo: function(info) { + if (this.children.length > 0) { + for (var i = 0; i < this.children.length; ++i) + this.children[i].collectSummaryInfo(info); + } else if (this.data && !this.data.hasChildren) { + if (this.data.type == 'cookie') { + info.cookies++; + } else if (this.data.type == 'database') { + info.database = true; + } else if (this.data.type == 'local_storage') { + info.localStorage = true; + } else if (this.data.type == 'app_cache') { + info.appCache = true; + } else if (this.data.type == 'indexed_db') { + info.indexedDb = true; + } else if (this.data.type == 'file_system') { + info.fileSystem = true; + } else if (this.data.type == 'quota') { + info.quota = this.data; + } else if (this.data.type == 'server_bound_cert') { + info.serverBoundCerts++; + } else if (this.data.type == 'flash_lso') { + info.flashLSO = true; + } + + var apps = this.data.appsProtectingThis; + if (apps) { + if (!info.appsProtectingThis) + info.appsProtectingThis = {}; + apps.forEach(function(appInfo) { + info.appsProtectingThis[appInfo.id] = appInfo; + }); + } + } + }, + + /** + * Create the cookie "bubbles" for this node, recursing into children + * if there are any. Append the cookie bubbles to @{code item}. + * @param {CookieListItem} item The cookie list item to create items in. + */ + createItems: function(item) { + if (this.children.length > 0) { + for (var i = 0; i < this.children.length; ++i) + this.children[i].createItems(item); + return; + } + + if (!this.data || this.data.hasChildren) + return; + + var text = ''; + switch (this.data.type) { + case 'cookie': + case 'database': + text = this.data.name; + break; + default: + text = loadTimeData.getString('cookie_' + this.data.type); + } + if (!text) + return; + + var div = item.ownerDocument.createElement('div'); + div.className = 'cookie-item'; + // Help out screen readers and such: this is a clickable thing. + div.setAttribute('role', 'button'); + div.tabIndex = 0; + div.textContent = text; + var apps = this.data.appsProtectingThis; + if (apps) + apps.forEach(addAppInfo.bind(null, div)); + + var index = item.appendItem(this, div); + div.onclick = function() { + item.selectedIndex = (item.selectedIndex == index) ? -1 : index; + }; + }, + + /** + * Set the detail text to be displayed to that of this cookie tree node. + * Uses preallocated DOM elements for each cookie node type from @{code + * infoNodes}, and inserts the appropriate elements to @{code element}. + * @param {Element} element The DOM element to insert elements to. + * @param {Object.<string, {table: Element, info: Object.<string, + * Element>}>} infoNodes The map from cookie node types to maps from + * cookie attribute names to DOM elements to display cookie attribute + * values, created by @{code CookiesList.decorate}. + */ + setDetailText: function(element, infoNodes) { + var table; + if (this.data && !this.data.hasChildren && cookieInfo[this.data.type]) { + var info = cookieInfo[this.data.type]; + var nodes = infoNodes[this.data.type].info; + for (var i = 0; i < info.length; ++i) { + var name = info[i][0]; + if (name != 'id' && this.data[name]) + nodes[name].textContent = this.data[name]; + else + nodes[name].textContent = ''; + } + table = infoNodes[this.data.type].table; + } + + while (element.childNodes.length > 1) + element.removeChild(element.firstChild); + + if (table) + element.insertBefore(table, element.firstChild); + }, + + /** + * The parent of this cookie tree node. + * @type {?CookieTreeNode|CookieListItem} + */ + get parent() { + // See below for an explanation of this special case. + if (typeof this.parent_ == 'number') + return this.list_.getListItemByIndex(this.parent_); + return this.parent_; + }, + set parent(parent) { + if (parent == this.parent) + return; + + if (parent instanceof CookieListItem) { + // If the parent is to be a CookieListItem, then we keep the reference + // to it by its containing list and list index, rather than directly. + // This allows the list items to be garbage collected when they scroll + // out of view (except the expanded item, which we cache). This is + // transparent except in the setter and getter, where we handle it. + if (this.parent_ == undefined || parent.listIndex != -1) { + // Setting the parent is somewhat tricky because the CookieListItem + // constructor has side-effects on the |origin| that it wraps. Every + // time a CookieListItem is created for an |origin|, it registers + // itself as the parent of the |origin|. + // The List implementation may create a temporary CookieListItem item + // that wraps the |origin| of the very first entry of the CokiesList, + // when the List is redrawn the first time. This temporary + // CookieListItem is fresh (has listIndex = -1) and is never inserted + // into the List. Therefore it gets never updated. This destroys the + // chain of parent pointers. + // This is the stack trace: + // CookieListItem + // CookiesList.createItem + // List.measureItem + // List.getDefaultItemSize_ + // List.getDefaultItemHeight_ + // List.getIndexForListOffset_ + // List.getItemsInViewPort + // List.redraw + // List.endBatchUpdates + // CookiesList.loadChildren + this.parent_ = parent.listIndex; + } + this.list_ = parent.list; + parent.addEventListener('listIndexChange', + this.parentIndexChanged_.bind(this)); + } else { + this.parent_ = parent; + } + + if (this.data && this.data.id) { + if (parent) + parentLookup[this.data.id] = this; + else + delete parentLookup[this.data.id]; + } + + if (this.data && this.data.hasChildren && + !this.children.length && !lookupRequests[this.data.id]) { + lookupRequests[this.data.id] = true; + chrome.send('loadCookie', [this.pathId]); + } + }, + + /** + * Called when the parent is a CookieListItem whose index has changed. + * See the code above that avoids keeping a direct reference to + * CookieListItem parents, to allow them to be garbage collected. + * @private + */ + parentIndexChanged_: function(event) { + if (typeof this.parent_ == 'number') { + this.parent_ = event.newValue; + // We set a timeout to update the origin, rather than doing it right + // away, because this callback may occur while the list items are + // being repopulated following a scroll event. Calling updateOrigin() + // immediately could trigger relayout that would reset the scroll + // position within the list, among other things. + window.setTimeout(this.updateOrigin.bind(this), 0); + } + }, + + /** + * The cookie tree path id. + * @type {string} + */ + get pathId() { + var parent = this.parent; + if (parent && parent instanceof CookieTreeNode) + return parent.pathId + ',' + this.data.id; + return this.data.id; + }, + }; + + /** + * Creates a new cookies list. + * @param {Object=} opt_propertyBag Optional properties. + * @constructor + * @extends {DeletableItemList} + */ + var CookiesList = cr.ui.define('list'); + + CookiesList.prototype = { + __proto__: DeletableItemList.prototype, + + /** @override */ + decorate: function() { + DeletableItemList.prototype.decorate.call(this); + this.classList.add('cookie-list'); + this.dataModel = new ArrayDataModel([]); + this.addEventListener('keydown', this.handleKeyLeftRight_.bind(this)); + var sm = new ListSingleSelectionModel(); + sm.addEventListener('change', this.cookieSelectionChange_.bind(this)); + sm.addEventListener('leadIndexChange', this.cookieLeadChange_.bind(this)); + this.selectionModel = sm; + this.infoNodes = {}; + this.fixedHeight = false; + var doc = this.ownerDocument; + // Create a table for each type of site data (e.g. cookies, databases, + // etc.) and save it so that we can reuse it for all origins. + for (var type in cookieInfo) { + var table = doc.createElement('table'); + table.className = 'cookie-details-table'; + var tbody = doc.createElement('tbody'); + table.appendChild(tbody); + var info = {}; + for (var i = 0; i < cookieInfo[type].length; i++) { + var tr = doc.createElement('tr'); + var name = doc.createElement('td'); + var data = doc.createElement('td'); + var pair = cookieInfo[type][i]; + name.className = 'cookie-details-label'; + name.textContent = loadTimeData.getString(pair[1]); + data.className = 'cookie-details-value'; + data.textContent = ''; + tr.appendChild(name); + tr.appendChild(data); + tbody.appendChild(tr); + info[pair[0]] = data; + } + this.infoNodes[type] = {table: table, info: info}; + } + }, + + /** + * Handles key down events and looks for left and right arrows, then + * dispatches to the currently expanded item, if any. + * @param {Event} e The keydown event. + * @private + */ + handleKeyLeftRight_: function(e) { + var id = e.keyIdentifier; + if ((id == 'Left' || id == 'Right') && this.expandedItem) { + var cs = this.ownerDocument.defaultView.getComputedStyle(this); + var rtl = cs.direction == 'rtl'; + if ((!rtl && id == 'Left') || (rtl && id == 'Right')) + this.expandedItem.selectedIndex--; + else + this.expandedItem.selectedIndex++; + this.scrollIndexIntoView(this.expandedItem.listIndex); + // Prevent the page itself from scrolling. + e.preventDefault(); + } + }, + + /** + * Called on selection model selection changes. + * @param {Event} ce The selection change event. + * @private + */ + cookieSelectionChange_: function(ce) { + ce.changes.forEach(function(change) { + var listItem = this.getListItemByIndex(change.index); + if (listItem) { + if (!change.selected) { + // We set a timeout here, rather than setting the item unexpanded + // immediately, so that if another item gets set expanded right + // away, it will be expanded before this item is unexpanded. It + // will notice that, and unexpand this item in sync with its own + // expansion. Later, this callback will end up having no effect. + window.setTimeout(function() { + if (!listItem.selected || !listItem.lead) + listItem.expanded = false; + }, 0); + } else if (listItem.lead) { + listItem.expanded = true; + } + } + }, this); + }, + + /** + * Called on selection model lead changes. + * @param {Event} pe The lead change event. + * @private + */ + cookieLeadChange_: function(pe) { + if (pe.oldValue != -1) { + var listItem = this.getListItemByIndex(pe.oldValue); + if (listItem) { + // See cookieSelectionChange_ above for why we use a timeout here. + window.setTimeout(function() { + if (!listItem.lead || !listItem.selected) + listItem.expanded = false; + }, 0); + } + } + if (pe.newValue != -1) { + var listItem = this.getListItemByIndex(pe.newValue); + if (listItem && listItem.selected) + listItem.expanded = true; + } + }, + + /** + * The currently expanded item. Used by CookieListItem above. + * @type {?CookieListItem} + */ + expandedItem: null, + + // from cr.ui.List + /** @override */ + createItem: function(data) { + // We use the cached expanded item in order to allow it to maintain some + // state (like its fixed height, and which bubble is selected). + if (this.expandedItem && this.expandedItem.origin == data) + return this.expandedItem; + return new CookieListItem(data, this); + }, + + // from options.DeletableItemList + /** @override */ + deleteItemAtIndex: function(index) { + var item = this.dataModel.item(index); + if (item) { + var pathId = item.pathId; + if (pathId) + chrome.send('removeCookie', [pathId]); + } + }, + + /** + * Insert the given list of cookie tree nodes at the given index. + * Both CookiesList and CookieTreeNode implement this API. + * @param {Array.<Object>} data The data objects for the nodes to add. + * @param {number} start The index at which to start inserting the nodes. + */ + insertAt: function(data, start) { + spliceTreeNodes(data, start, this.dataModel); + }, + + /** + * Remove a cookie tree node from the given index. + * Both CookiesList and CookieTreeNode implement this API. + * @param {number} index The index of the tree node to remove. + */ + remove: function(index) { + if (index < this.dataModel.length) + this.dataModel.splice(index, 1); + }, + + /** + * Clears the list. + * Both CookiesList and CookieTreeNode implement this API. + * It is used by CookiesList.loadChildren(). + */ + clear: function() { + parentLookup = {}; + this.dataModel.splice(0, this.dataModel.length); + this.redraw(); + }, + + /** + * Add tree nodes by given parent. + * @param {Object} parent The parent node. + * @param {number} start The index at which to start inserting the nodes. + * @param {Array} nodesData Nodes data array. + * @private + */ + addByParent_: function(parent, start, nodesData) { + if (!parent) + return; + + parent.startBatchUpdates(); + parent.insertAt(nodesData, start); + parent.endBatchUpdates(); + + cr.dispatchSimpleEvent(this, 'change'); + }, + + /** + * Add tree nodes by parent id. + * This is used by cookies_view.js. + * @param {string} parentId Id of the parent node. + * @param {number} start The index at which to start inserting the nodes. + * @param {Array} nodesData Nodes data array. + */ + addByParentId: function(parentId, start, nodesData) { + var parent = parentId ? parentLookup[parentId] : this; + this.addByParent_(parent, start, nodesData); + }, + + /** + * Removes tree nodes by parent id. + * This is used by cookies_view.js. + * @param {string} parentId Id of the parent node. + * @param {number} start The index at which to start removing the nodes. + * @param {number} count Number of nodes to remove. + */ + removeByParentId: function(parentId, start, count) { + var parent = parentId ? parentLookup[parentId] : this; + if (!parent) + return; + + parent.startBatchUpdates(); + while (count-- > 0) + parent.remove(start); + parent.endBatchUpdates(); + + cr.dispatchSimpleEvent(this, 'change'); + }, + + /** + * Loads the immediate children of given parent node. + * This is used by cookies_view.js. + * @param {string} parentId Id of the parent node. + * @param {Array} children The immediate children of parent node. + */ + loadChildren: function(parentId, children) { + if (parentId) + delete lookupRequests[parentId]; + var parent = parentId ? parentLookup[parentId] : this; + if (!parent) + return; + + parent.startBatchUpdates(); + parent.clear(); + this.addByParent_(parent, 0, children); + parent.endBatchUpdates(); + }, + }; + + return { + CookiesList: CookiesList + }; +}); diff --git a/chromium/chrome/browser/resources/options/cookies_view.css b/chromium/chrome/browser/resources/options/cookies_view.css new file mode 100644 index 00000000000..ab7a62c1c7f --- /dev/null +++ b/chromium/chrome/browser/resources/options/cookies_view.css @@ -0,0 +1,198 @@ +/* Copyright (c) 2012 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ + +/* Styles for the cookies list page. */ +.cookies-view-page { + height: 90%; + margin-left: -15px; + width: 720px; +} + +/* Styles for the cookies list elements in cookies_view.html. */ +.cookies-list { + -webkit-box-flex: 1; + /* This property overrides the |min-height: 192px;| property above due to + * special behavior of the cookies list. */ + border: 1px solid #D9D9D9; + margin: 0; + margin-top: 5px; + min-height: 0; +} + +.cookies-list-content-area { + -webkit-box-orient: vertical; + display: -webkit-box; + overflow-y: hidden; +} + +.cookies-column-headers { + -webkit-box-align: baseline; + -webkit-box-orient: horizontal; + display: -webkit-box; + position: relative; + width: 100%; +} + +.cookies-column-headers > * { + display: block; +} + +.cookies-column-headers h3 { + font-size: 105%; + font-weight: bold; + margin: 10px 0; +} + +/* Notice the width and padding for these columns match up with those below. */ +.cookies-site-column { + -webkit-padding-start: 7px; + width: 14em; +} + +.cookies-data-column { + -webkit-box-flex: 1; + -webkit-padding-start: 7px; +} + +/* Enable animating the height of items. */ +list.cookie-list .deletable-item { + -webkit-transition: height 150ms ease-in-out; +} + +/* Disable webkit-box display. */ +list.cookie-list .deletable-item > :first-child { + display: block; +} + +/* Force the X for deleting an origin to stay at the top. */ +list.cookie-list > .deletable-item > .close-button { + position: absolute; + right: 2px; + top: 8px; +} + +html[dir=rtl] list.cookie-list > .deletable-item > .close-button { + left: 2px; + right: auto; +} + +/* Styles for the site (aka origin) and its summary. */ +.cookie-site { + /* Notice that the width, margin, and padding match up with those above. */ + -webkit-margin-end: 2px; + -webkit-padding-start: 5px; + display: inline-block; + overflow: hidden; + text-overflow: ellipsis; + width: 14em; +} + +list.cookie-list > .deletable-item[selected] .cookie-site { + -webkit-user-select: text; +} + +.cookie-data { + display: inline-block; + max-width: 410px; + overflow: hidden; + text-overflow: ellipsis; +} + +.cookie-size { + display: inline-block; + float: right; + margin-right: 0; +} + +list.cookie-list > .deletable-item[selected] .cookie-data { + -webkit-user-select: text; +} + + +/* Styles for the individual items (cookies, etc.). */ +.cookie-items { + /* Notice that the margin and padding match up with those above. */ + -webkit-margin-start: 14em; + -webkit-padding-start: 7px; + -webkit-transition: 150ms ease-in-out; + height: 0; + opacity: 0; + /* Make the cookie items wrap correctly. */ + white-space: normal; +} + +.measure-items .cookie-items { + -webkit-transition: none; + height: auto; + visibility: hidden; +} + +.show-items .cookie-items { + opacity: 1; +} + +.cookie-items .cookie-item { + background: rgb(224, 233, 245); + border: 1px solid rgb(131, 146, 174); + border-radius: 5px; + display: inline-block; + font-size: 85%; + height: auto; + margin: 2px 4px 2px 0; + max-width: 120px; + min-width: 40px; + overflow: hidden; + padding: 0 3px; + text-align: center; + text-overflow: ellipsis; +} + +.cookie-items .cookie-item:hover { + background: rgb(238, 243, 249); + border-color: rgb(100, 113, 135); +} + +.cookie-items .cookie-item[selected] { + background: rgb(245, 248, 248); + border-color: #B2B2B2; +} + +.cookie-items .cookie-item[selected]:hover { + background: rgb(245, 248, 248); + border-color: rgb(100, 113, 135); +} + +.cookie-items .cookie-item .protecting-app, +.cookie-data .protecting-app { + margin-bottom: -3px; + margin-left: 4px; +} + +/* Styles for the cookie details box. */ +.cookie-details { + background: rgb(245, 248, 248); + border: 1px solid #B2B2B2; + border-radius: 5px; + margin-top: 2px; + padding: 5px; +} + +list.cookie-list > .deletable-item[selected] .cookie-details { + -webkit-user-select: text; +} + +.cookie-details-table { + table-layout: fixed; + width: 100%; +} + +.cookie-details-label { + vertical-align: top; + white-space: pre; + width: 10em; +} + +.cookie-details-value { + word-wrap: break-word; +} diff --git a/chromium/chrome/browser/resources/options/cookies_view.html b/chromium/chrome/browser/resources/options/cookies_view.html new file mode 100644 index 00000000000..0cc136b6630 --- /dev/null +++ b/chromium/chrome/browser/resources/options/cookies_view.html @@ -0,0 +1,28 @@ +<div id="cookies-view-page" class="page cookies-view-page" hidden> + <div class="close-button"></div> + <h1 i18n-content="cookiesViewPage"></h1> + <div class="content-area cookies-list-content-area"> + <div class="cookies-column-headers"> + <div class="cookies-site-column"> + <h3 i18n-content="cookie_domain"></h3> + </div> + <div class="cookies-data-column"> + <h3 i18n-content="cookie_local_data"></h3> + </div> + <div class="cookies-header-controls"> + <button class="remove-all-cookies-button" + i18n-content="remove_all_cookie"></button> + <input type="search" class="cookies-search-box" + i18n-values="placeholder:search_cookies" incremental> + </div> + </div> + <list id="cookies-list" class="cookies-list"></list> + </div> + <div class="action-area"> + <div class="button-strip"> + <button class="cookies-view-overlay-confirm default-button" + i18n-content="done"> + </button> + </div> + </div> +</div> diff --git a/chromium/chrome/browser/resources/options/cookies_view.js b/chromium/chrome/browser/resources/options/cookies_view.js new file mode 100644 index 00000000000..4d496684c42 --- /dev/null +++ b/chromium/chrome/browser/resources/options/cookies_view.js @@ -0,0 +1,140 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + + var OptionsPage = options.OptionsPage; + + ///////////////////////////////////////////////////////////////////////////// + // CookiesView class: + + /** + * Encapsulated handling of the cookies and other site data page. + * @constructor + */ + function CookiesView(model) { + OptionsPage.call(this, 'cookies', + loadTimeData.getString('cookiesViewPageTabTitle'), + 'cookies-view-page'); + } + + cr.addSingletonGetter(CookiesView); + + CookiesView.prototype = { + __proto__: OptionsPage.prototype, + + /** + * The timer id of the timer set on search query change events. + * @type {number} + * @private + */ + queryDelayTimerId_: 0, + + /** + * The most recent search query, empty string if the query is empty. + * @type {string} + * @private + */ + lastQuery_: '', + + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + + var searchBox = this.pageDiv.querySelector('.cookies-search-box'); + searchBox.addEventListener( + 'search', this.handleSearchQueryChange_.bind(this)); + searchBox.onkeydown = function(e) { + // Prevent the overlay from handling this event. + if (e.keyIdentifier == 'Enter') + e.stopPropagation(); + }; + + this.pageDiv.querySelector('.remove-all-cookies-button').onclick = + function(e) { + chrome.send('removeAllCookies'); + }; + + var cookiesList = this.pageDiv.querySelector('.cookies-list'); + options.CookiesList.decorate(cookiesList); + + this.addEventListener('visibleChange', this.handleVisibleChange_); + + this.pageDiv.querySelector('.cookies-view-overlay-confirm').onclick = + OptionsPage.closeOverlay.bind(OptionsPage); + }, + + /** @override */ + didShowPage: function() { + this.pageDiv.querySelector('.cookies-search-box').value = ''; + this.lastQuery_ = ''; + }, + + /** + * Search cookie using text in |cookies-search-box|. + */ + searchCookie: function() { + this.queryDelayTimerId_ = 0; + var filter = this.pageDiv.querySelector('.cookies-search-box').value; + if (this.lastQuery_ != filter) { + this.lastQuery_ = filter; + chrome.send('updateCookieSearchResults', [filter]); + } + }, + + /** + * Handles search query changes. + * @param {!Event} e The event object. + * @private + */ + handleSearchQueryChange_: function(e) { + if (this.queryDelayTimerId_) + window.clearTimeout(this.queryDelayTimerId_); + + this.queryDelayTimerId_ = window.setTimeout( + this.searchCookie.bind(this), 500); + }, + + initialized_: false, + + /** + * Handler for OptionsPage's visible property change event. + * @param {Event} e Property change event. + * @private + */ + handleVisibleChange_: function(e) { + if (!this.visible) + return; + + chrome.send('reloadCookies'); + + if (!this.initialized_) { + this.initialized_ = true; + this.searchCookie(); + } else { + this.pageDiv.querySelector('.cookies-list').redraw(); + } + + this.pageDiv.querySelector('.cookies-search-box').focus(); + }, + }; + + // CookiesViewHandler callbacks. + CookiesView.onTreeItemAdded = function(args) { + $('cookies-list').addByParentId(args[0], args[1], args[2]); + }; + + CookiesView.onTreeItemRemoved = function(args) { + $('cookies-list').removeByParentId(args[0], args[1], args[2]); + }; + + CookiesView.loadChildren = function(args) { + $('cookies-list').loadChildren(args[0], args[1]); + }; + + // Export + return { + CookiesView: CookiesView + }; + +}); diff --git a/chromium/chrome/browser/resources/options/deletable_item_list.js b/chromium/chrome/browser/resources/options/deletable_item_list.js new file mode 100644 index 00000000000..403dc0fc1dc --- /dev/null +++ b/chromium/chrome/browser/resources/options/deletable_item_list.js @@ -0,0 +1,196 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + /** @const */ var List = cr.ui.List; + /** @const */ var ListItem = cr.ui.ListItem; + + /** + * Creates a deletable list item, which has a button that will trigger a call + * to deleteItemAtIndex(index) in the list. + */ + var DeletableItem = cr.ui.define('li'); + + DeletableItem.prototype = { + __proto__: ListItem.prototype, + + /** + * The element subclasses should populate with content. + * @type {HTMLElement} + * @private + */ + contentElement_: null, + + /** + * The close button element. + * @type {HTMLElement} + * @private + */ + closeButtonElement_: null, + + /** + * Whether or not this item can be deleted. + * @type {boolean} + * @private + */ + deletable_: true, + + /** @override */ + decorate: function() { + ListItem.prototype.decorate.call(this); + + this.classList.add('deletable-item'); + + this.contentElement_ = this.ownerDocument.createElement('div'); + this.appendChild(this.contentElement_); + + this.closeButtonElement_ = this.ownerDocument.createElement('button'); + this.closeButtonElement_.className = + 'raw-button row-delete-button custom-appearance'; + this.closeButtonElement_.addEventListener('mousedown', + this.handleMouseDownUpOnClose_); + this.closeButtonElement_.addEventListener('mouseup', + this.handleMouseDownUpOnClose_); + this.closeButtonElement_.addEventListener('focus', + this.handleFocus_.bind(this)); + this.appendChild(this.closeButtonElement_); + }, + + /** + * Returns the element subclasses should add content to. + * @return {HTMLElement} The element subclasses should popuplate. + */ + get contentElement() { + return this.contentElement_; + }, + + /** + * Returns the close button element. + * @return {HTMLElement} The close |<button>| element. + */ + get closeButtonElement() { + return this.closeButtonElement_; + }, + + /* Gets/sets the deletable property. An item that is not deletable doesn't + * show the delete button (although space is still reserved for it). + */ + get deletable() { + return this.deletable_; + }, + set deletable(value) { + this.deletable_ = value; + this.closeButtonElement_.disabled = !value; + }, + + /** + * Called when a focusable child element receives focus. Selects this item + * in the list selection model. + * @private + */ + handleFocus_: function() { + var list = this.parentNode; + var index = list.getIndexOfListItem(this); + list.selectionModel.selectedIndex = index; + list.selectionModel.anchorIndex = index; + }, + + /** + * Don't let the list have a crack at the event. We don't want clicking the + * close button to change the selection of the list or to focus on the close + * button. + * @param {Event} e The mouse down/up event object. + * @private + */ + handleMouseDownUpOnClose_: function(e) { + if (e.target.disabled) + return; + e.stopPropagation(); + e.preventDefault(); + }, + }; + + var DeletableItemList = cr.ui.define('list'); + + DeletableItemList.prototype = { + __proto__: List.prototype, + + /** @override */ + decorate: function() { + List.prototype.decorate.call(this); + this.addEventListener('click', this.handleClick_); + this.addEventListener('keydown', this.handleKeyDown_); + }, + + /** + * Callback for onclick events. + * @param {Event} e The click event object. + * @private + */ + handleClick_: function(e) { + if (this.disabled) + return; + + var target = e.target; + if (target.classList.contains('row-delete-button')) { + var listItem = this.getListItemAncestor(target); + var selected = this.selectionModel.selectedIndexes; + + // Check if the list item that contains the close button being clicked + // is not in the list of selected items. Only delete this item in that + // case. + var idx = this.getIndexOfListItem(listItem); + if (selected.indexOf(idx) == -1) { + this.deleteItemAtIndex(idx); + } else { + this.deleteSelectedItems_(); + } + } + }, + + /** + * Callback for keydown events. + * @param {Event} e The keydown event object. + * @private + */ + handleKeyDown_: function(e) { + // Map delete (and backspace on Mac) to item deletion (unless focus is + // in an input field, where it's intended for text editing). + if ((e.keyCode == 46 || (e.keyCode == 8 && cr.isMac)) && + e.target.tagName != 'INPUT') { + this.deleteSelectedItems_(); + // Prevent the browser from going back. + e.preventDefault(); + } + }, + + /** + * Deletes all the currently selected items that are deletable. + * @private + */ + deleteSelectedItems_: function() { + var selected = this.selectionModel.selectedIndexes; + // Reverse through the list of selected indexes to maintain the + // correct index values after deletion. + for (var j = selected.length - 1; j >= 0; j--) { + var index = selected[j]; + if (this.getListItemByIndex(index).deletable) + this.deleteItemAtIndex(index); + } + }, + + /** + * Called when an item should be deleted; subclasses are responsible for + * implementing. + * @param {number} index The index of the item that is being deleted. + */ + deleteItemAtIndex: function(index) { + }, + }; + + return { + DeletableItemList: DeletableItemList, + DeletableItem: DeletableItem, + }; +}); diff --git a/chromium/chrome/browser/resources/options/do_not_track_confirm_overlay.css b/chromium/chrome/browser/resources/options/do_not_track_confirm_overlay.css new file mode 100644 index 00000000000..96950c10265 --- /dev/null +++ b/chromium/chrome/browser/resources/options/do_not_track_confirm_overlay.css @@ -0,0 +1,7 @@ +/* Copyright (c) 2012 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ + +#do-not-track-confirm-overlay { + width: 500px; +} diff --git a/chromium/chrome/browser/resources/options/do_not_track_confirm_overlay.html b/chromium/chrome/browser/resources/options/do_not_track_confirm_overlay.html new file mode 100644 index 00000000000..e39d00f0af1 --- /dev/null +++ b/chromium/chrome/browser/resources/options/do_not_track_confirm_overlay.html @@ -0,0 +1,23 @@ +<div id="do-not-track-confirm-overlay" class="page" hidden> + <div class="close-button"></div> + <h1 i18n-content="doNotTrackConfirmOverlay"></h1> + <div class="content-area"> + <span id="do-not-track-confirm-text" + i18n-content="doNotTrackConfirmMessage"> + </span> + <a target="_blank" i18n-content="learnMore" + i18n-values="href:doNotTrackLearnMoreURL"> + </a> + </div> + <div class="action-area"> + <div class="button-strip"> + <button id="do-not-track-confirm-ok" class="default-button" + i18n-content="doNotTrackConfirmEnable"> + </button> + <button id="do-not-track-confirm-cancel" + i18n-content="doNotTrackConfirmDisable" + class="cancel-button"> + </button> + </div> + </div> +</div> diff --git a/chromium/chrome/browser/resources/options/editable_text_field.js b/chromium/chrome/browser/resources/options/editable_text_field.js new file mode 100644 index 00000000000..8c5787249cc --- /dev/null +++ b/chromium/chrome/browser/resources/options/editable_text_field.js @@ -0,0 +1,375 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + var EditableTextField = cr.ui.define('div'); + + /** + * Decorates an element as an editable text field. + * @param {!HTMLElement} el The element to decorate. + */ + EditableTextField.decorate = function(el) { + el.__proto__ = EditableTextField.prototype; + el.decorate(); + }; + + EditableTextField.prototype = { + __proto__: HTMLDivElement.prototype, + + /** + * The actual input element in this field. + * @type {?HTMLElement} + * @private + */ + editField_: null, + + /** + * The static text displayed when this field isn't editable. + * @type {?HTMLElement} + * @private + */ + staticText_: null, + + /** + * The data model for this field. + * @type {?Object} + * @private + */ + model_: null, + + /** + * Whether or not the current edit should be considered canceled, rather + * than committed, when editing ends. + * @type {boolean} + * @private + */ + editCanceled_: true, + + /** @override */ + decorate: function() { + this.classList.add('editable-text-field'); + + this.createEditableTextCell(); + + if (this.hasAttribute('i18n-placeholder-text')) { + var identifier = this.getAttribute('i18n-placeholder-text'); + var localizedText = loadTimeData.getString(identifier); + if (localizedText) + this.setAttribute('placeholder-text', localizedText); + } + + this.addEventListener('keydown', this.handleKeyDown_); + this.editField_.addEventListener('focus', this.handleFocus_.bind(this)); + this.editField_.addEventListener('blur', this.handleBlur_.bind(this)); + this.checkForEmpty_(); + }, + + /** + * Indicates that this field has no value in the model, and the placeholder + * text (if any) should be shown. + * @type {boolean} + */ + get empty() { + return this.hasAttribute('empty'); + }, + + /** + * The placeholder text to be used when the model or its value is empty. + * @type {string} + */ + get placeholderText() { + return this.getAttribute('placeholder-text'); + }, + set placeholderText(text) { + if (text) + this.setAttribute('placeholder-text', text); + else + this.removeAttribute('placeholder-text'); + + this.checkForEmpty_(); + }, + + /** + * Returns the input element in this text field. + * @type {HTMLElement} The element that is the actual input field. + */ + get editField() { + return this.editField_; + }, + + /** + * Whether the user is currently editing the list item. + * @type {boolean} + */ + get editing() { + return this.hasAttribute('editing'); + }, + set editing(editing) { + if (this.editing == editing) + return; + + if (editing) + this.setAttribute('editing', ''); + else + this.removeAttribute('editing'); + + if (editing) { + this.editCanceled_ = false; + + if (this.empty) { + this.removeAttribute('empty'); + if (this.editField) + this.editField.value = ''; + } + if (this.editField) { + this.editField.focus(); + this.editField.select(); + } + } else { + if (!this.editCanceled_ && this.hasBeenEdited && + this.currentInputIsValid) { + this.updateStaticValues_(); + cr.dispatchSimpleEvent(this, 'commitedit', true); + } else { + this.resetEditableValues_(); + cr.dispatchSimpleEvent(this, 'canceledit', true); + } + this.checkForEmpty_(); + } + }, + + /** + * Whether the item is editable. + * @type {boolean} + */ + get editable() { + return this.hasAttribute('editable'); + }, + set editable(editable) { + if (this.editable == editable) + return; + + if (editable) + this.setAttribute('editable', ''); + else + this.removeAttribute('editable'); + this.editable_ = editable; + }, + + /** + * The data model for this field. + * @type {Object} + */ + get model() { + return this.model_; + }, + set model(model) { + this.model_ = model; + this.checkForEmpty_(); // This also updates the editField value. + this.updateStaticValues_(); + }, + + /** + * The HTML element that should have focus initially when editing starts, + * if a specific element wasn't clicked. Defaults to the first <input> + * element; can be overridden by subclasses if a different element should be + * focused. + * @type {?HTMLElement} + */ + get initialFocusElement() { + return this.querySelector('input'); + }, + + /** + * Whether the input in currently valid to submit. If this returns false + * when editing would be submitted, either editing will not be ended, + * or it will be cancelled, depending on the context. Can be overridden by + * subclasses to perform input validation. + * @type {boolean} + */ + get currentInputIsValid() { + return true; + }, + + /** + * Returns true if the item has been changed by an edit. Can be overridden + * by subclasses to return false when nothing has changed to avoid + * unnecessary commits. + * @type {boolean} + */ + get hasBeenEdited() { + return true; + }, + + /** + * Mutates the input during a successful commit. Can be overridden to + * provide a way to "clean up" valid input so that it conforms to a + * desired format. Will only be called when commit succeeds for valid + * input, or when the model is set. + * @param {string} value Input text to be mutated. + * @return {string} mutated text. + */ + mutateInput: function(value) { + return value; + }, + + /** + * Creates a div containing an <input>, as well as static text, keeping + * references to them so they can be manipulated. + * @param {string} text The text of the cell. + * @private + */ + createEditableTextCell: function(text) { + // This function should only be called once. + if (this.editField_) + return; + + var container = this.ownerDocument.createElement('div'); + + var textEl = this.ownerDocument.createElement('div'); + textEl.className = 'static-text'; + textEl.textContent = text; + textEl.setAttribute('displaymode', 'static'); + this.appendChild(textEl); + this.staticText_ = textEl; + + var inputEl = this.ownerDocument.createElement('input'); + inputEl.className = 'editable-text'; + inputEl.type = 'text'; + inputEl.value = text; + inputEl.setAttribute('displaymode', 'edit'); + inputEl.staticVersion = textEl; + this.appendChild(inputEl); + this.editField_ = inputEl; + }, + + /** + * Resets the editable version of any controls created by + * createEditableTextCell to match the static text. + * @private + */ + resetEditableValues_: function() { + var editField = this.editField_; + var staticLabel = editField.staticVersion; + if (!staticLabel) + return; + + if (editField instanceof HTMLInputElement) + editField.value = staticLabel.textContent; + + editField.setCustomValidity(''); + }, + + /** + * Sets the static version of any controls created by createEditableTextCell + * to match the current value of the editable version. Called on commit so + * that there's no flicker of the old value before the model updates. Also + * updates the model's value with the mutated value of the edit field. + * @private + */ + updateStaticValues_: function() { + var editField = this.editField_; + var staticLabel = editField.staticVersion; + if (!staticLabel) + return; + + if (editField instanceof HTMLInputElement) { + staticLabel.textContent = editField.value; + this.model_.value = this.mutateInput(editField.value); + } + }, + + /** + * Checks to see if the model or its value are empty. If they are, then set + * the edit field to the placeholder text, if any, and if not, set it to the + * model's value. + * @private + */ + checkForEmpty_: function() { + var editField = this.editField_; + if (!editField) + return; + + if (!this.model_ || !this.model_.value) { + this.setAttribute('empty', ''); + editField.value = this.placeholderText || ''; + } else { + this.removeAttribute('empty'); + editField.value = this.model_.value; + } + }, + + /** + * Called when this widget receives focus. + * @param {Event} e the focus event. + * @private + */ + handleFocus_: function(e) { + if (this.editing) + return; + + this.editing = true; + if (this.editField_) + this.editField_.focus(); + }, + + /** + * Called when this widget loses focus. + * @param {Event} e the blur event. + * @private + */ + handleBlur_: function(e) { + if (!this.editing) + return; + + this.editing = false; + }, + + /** + * Called when a key is pressed. Handles committing and canceling edits. + * @param {Event} e The key down event. + * @private + */ + handleKeyDown_: function(e) { + if (!this.editing) + return; + + var endEdit; + switch (e.keyIdentifier) { + case 'U+001B': // Esc + this.editCanceled_ = true; + endEdit = true; + break; + case 'Enter': + if (this.currentInputIsValid) + endEdit = true; + break; + } + + if (endEdit) { + // Blurring will trigger the edit to end. + this.ownerDocument.activeElement.blur(); + // Make sure that handled keys aren't passed on and double-handled. + // (e.g., esc shouldn't both cancel an edit and close a subpage) + e.stopPropagation(); + } + }, + }; + + /** + * Takes care of committing changes to EditableTextField items when the + * window loses focus. + */ + window.addEventListener('blur', function(e) { + var itemAncestor = findAncestor(document.activeElement, function(node) { + return node instanceof EditableTextField; + }); + if (itemAncestor) + document.activeElement.blur(); + }); + + return { + EditableTextField: EditableTextField, + }; +}); diff --git a/chromium/chrome/browser/resources/options/factory_reset_overlay.css b/chromium/chrome/browser/resources/options/factory_reset_overlay.css new file mode 100644 index 00000000000..c7369e3fa50 --- /dev/null +++ b/chromium/chrome/browser/resources/options/factory_reset_overlay.css @@ -0,0 +1,7 @@ +/* Copyright (c) 2012 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ + +#factory-reset-overlay { + max-width: 550px; +} diff --git a/chromium/chrome/browser/resources/options/factory_reset_overlay.html b/chromium/chrome/browser/resources/options/factory_reset_overlay.html new file mode 100644 index 00000000000..21df28f9403 --- /dev/null +++ b/chromium/chrome/browser/resources/options/factory_reset_overlay.html @@ -0,0 +1,16 @@ +<div id="factory-reset-overlay" class="page" hidden> + <div class="close-button"></div> + <h1 i18n-content="factoryResetHeading"></h1> + <div class="content-area"> + <span i18n-content="factoryResetWarning"></span> + <a i18n-values="href:factoryResetHelpUrl" + i18n-content="errorLearnMore" target="_blank"></a> + </div> + <div class="action-area button-strip"> + <button id="factory-reset-data-dismiss" i18n-content="cancel"> + </button> + <button id="factory-reset-data-restart" class="default-button" + i18n-content="factoryResetDataRestart"> + </button> + </div> +</div> diff --git a/chromium/chrome/browser/resources/options/factory_reset_overlay.js b/chromium/chrome/browser/resources/options/factory_reset_overlay.js new file mode 100644 index 00000000000..a3d3c6e5d74 --- /dev/null +++ b/chromium/chrome/browser/resources/options/factory_reset_overlay.js @@ -0,0 +1,49 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + var OptionsPage = options.OptionsPage; + + /** + * FactoryResetOverlay class + * Encapsulated handling of the Factory Reset confirmation overlay page. + * @class + */ + function FactoryResetOverlay() { + OptionsPage.call(this, 'factoryResetData', + loadTimeData.getString('factoryResetTitle'), + 'factory-reset-overlay'); + } + + cr.addSingletonGetter(FactoryResetOverlay); + + FactoryResetOverlay.prototype = { + // Inherit FactoryResetOverlay from OptionsPage. + __proto__: OptionsPage.prototype, + + /** + * Initialize the page. + */ + initializePage: function() { + // Call base class implementation to starts preference initialization. + OptionsPage.prototype.initializePage.call(this); + + $('factory-reset-data-dismiss').onclick = function(event) { + FactoryResetOverlay.dismiss(); + }; + $('factory-reset-data-restart').onclick = function(event) { + chrome.send('performFactoryResetRestart'); + }; + }, + }; + + FactoryResetOverlay.dismiss = function() { + OptionsPage.closeOverlay(); + }; + + // Export + return { + FactoryResetOverlay: FactoryResetOverlay + }; +}); diff --git a/chromium/chrome/browser/resources/options/font_settings.css b/chromium/chrome/browser/resources/options/font_settings.css new file mode 100644 index 00000000000..f999c586f2b --- /dev/null +++ b/chromium/chrome/browser/resources/options/font_settings.css @@ -0,0 +1,62 @@ +/* Copyright (c) 2012 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ + +#font-settings > section { + overflow: hidden; +} + +#font-settings .action-area { + -webkit-box-pack: start; +} + +#font-settings .action-area .spacer { + -webkit-box-flex: 1; + display: -webkit-box; +} + +#font-settings .button-strip { + width: 100%; +} + +.font-setting-container { + display: -webkit-box; +} + +#font-settings input[type='range'] { + width: 100%; +} + +#minimum-font-sample { + height: 35px; + overflow: hidden; + width: 270px; +} + +.font-input-div { + -webkit-margin-end: 3em; + width: 12em; +} + +.font-input-div > div > select { + margin-bottom: 10px; +} + +.font-input { + width: 100%; +} + +.font-sample-div { + direction: ltr; + height: 70px; + overflow: hidden; + width: 270px; +} + +.font-settings-huge { + float: right; +} + +html[dir=rtl] .font-settings-huge { + float: left; +} diff --git a/chromium/chrome/browser/resources/options/font_settings.html b/chromium/chrome/browser/resources/options/font_settings.html new file mode 100644 index 00000000000..da3106019e3 --- /dev/null +++ b/chromium/chrome/browser/resources/options/font_settings.html @@ -0,0 +1,113 @@ +<div id="font-settings" class="page" hidden> + <div class="close-button"></div> + <h1 i18n-content="fontSettingsPage"></h1> + <div class="content-area"> + <section> + <h3 i18n-content="fontSettingsStandard"></h3> + <div class="font-setting-container"> + <div class="font-input-div"> + <div> + <select id="standard-font-family" class="font-input" + data-type="string" metric="Options_ChangeStandardFont" + pref="webkit.webprefs.fonts.standard.Zyyy"> + </select> + </div> + <div> + <input id="standard-font-size" type="range" min="0" max="24" + pref="webkit.webprefs.default_font_size"> + <div> + <span i18n-content="fontSettingsSizeTiny"></span> + <span i18n-content="fontSettingsSizeHuge" + class="font-settings-huge"> + </span> + </div> + </div> + </div> + <div id="standard-font-sample" class="font-sample-div"></div> + </div> + </section> + <section> + <h3 i18n-content="fontSettingsSerif"></h3> + <div class="font-setting-container"> + <div class="font-input-div"> + <div> + <select id="serif-font-family" class="font-input" data-type="string" + pref="webkit.webprefs.fonts.serif.Zyyy" + metric="Options_ChangeSerifFont"> + </select> + </div> + </div> + <div id="serif-font-sample" class="font-sample-div"></div> + </div> + </section> + <section> + <h3 i18n-content="fontSettingsSansSerif"></h3> + <div class="font-setting-container"> + <div class="font-input-div"> + <div> + <select id="sans-serif-font-family" class="font-input" + data-type="string" metric="Options_ChangeSansSerifFont" + pref="webkit.webprefs.fonts.sansserif.Zyyy"> + </select> + </div> + </div> + <div id="sans-serif-font-sample" class="font-sample-div"></div> + </div> + </section> + <section> + <h3 i18n-content="fontSettingsFixedWidth"></h3> + <div class="font-setting-container"> + <div class="font-input-div"> + <div> + <select id="fixed-font-family" class="font-input" data-type="string" + pref="webkit.webprefs.fonts.fixed.Zyyy" + metric="Options_ChangeFixedFont"> + </select> + </div> + </div> + <div id="fixed-font-sample" class="font-sample-div"></div> + </div> + </section> + <section> + <h3 i18n-content="fontSettingsMinimumSize"></h3> + <div class="font-setting-container"> + <div class="font-input-div"> + <div> + <input id="minimum-font-size" type="range" min="0" max="15" + pref="webkit.webprefs.minimum_font_size"> + <div> + <span i18n-content="fontSettingsSizeTiny"></span> + <span i18n-content="fontSettingsSizeHuge" + class="font-settings-huge"> + </span> + </div> + </div> + </div> + <div id="minimum-font-sample" class="font-sample-div"></div> + </div> + </section> + <section> + <h3 i18n-content="fontSettingsEncoding"></h3> + <div class="font-input-div"> + <div> + <select id="font-encoding" data-type="string" + pref="intl.charset_default" + metric="Options_ChangeFontEncoding"> + </select> + </div> + </div> + </section> + </div> + <div class="action-area"> + <div class="button-strip"> + <span id="advanced-font-settings-install" hidden + i18n-values=".innerHTML:advancedFontSettingsInstall"></span> + <a id="advanced-font-settings-options" href="#" hidden + i18n-content="advancedFontSettingsOptions"></a> + <span class="spacer"></span> + <button id="font-settings-confirm" class="default-button" + i18n-content="done"> + </button> + </div> + </div> +</div> diff --git a/chromium/chrome/browser/resources/options/font_settings.js b/chromium/chrome/browser/resources/options/font_settings.js new file mode 100644 index 00000000000..0b3f3f70a9b --- /dev/null +++ b/chromium/chrome/browser/resources/options/font_settings.js @@ -0,0 +1,255 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + + var OptionsPage = options.OptionsPage; + + /** + * FontSettings class + * Encapsulated handling of the 'Fonts and Encoding' page. + * @class + */ + function FontSettings() { + OptionsPage.call(this, + 'fonts', + loadTimeData.getString('fontSettingsPageTabTitle'), + 'font-settings'); + } + + cr.addSingletonGetter(FontSettings); + + FontSettings.prototype = { + __proto__: OptionsPage.prototype, + + /** + * Initialize the page. + */ + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + + var standardFontRange = $('standard-font-size'); + standardFontRange.valueMap = [9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 20, + 22, 24, 26, 28, 30, 32, 34, 36, 40, 44, 48, 56, 64, 72]; + standardFontRange.addEventListener( + 'change', this.standardRangeChanged_.bind(this, standardFontRange)); + standardFontRange.customChangeHandler = + this.standardFontSizeChanged_.bind(standardFontRange); + + var minimumFontRange = $('minimum-font-size'); + minimumFontRange.valueMap = [6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, + 18, 20, 22, 24]; + minimumFontRange.addEventListener( + 'change', this.minimumRangeChanged_.bind(this, minimumFontRange)); + minimumFontRange.customChangeHandler = + this.minimumFontSizeChanged_.bind(minimumFontRange); + + var placeholder = loadTimeData.getString('fontSettingsPlaceholder'); + var elements = [$('standard-font-family'), $('serif-font-family'), + $('sans-serif-font-family'), $('fixed-font-family'), + $('font-encoding')]; + elements.forEach(function(el) { + el.appendChild(new Option(placeholder)); + el.setDisabled('noFontsAvailable', true); + }); + + $('font-settings-confirm').onclick = function() { + OptionsPage.closeOverlay(); + }; + + $('advanced-font-settings-options').onclick = function() { + chrome.send('openAdvancedFontSettingsOptions'); + }; + }, + + /** + * Called by the options page when this page has been shown. + */ + didShowPage: function() { + // The fonts list may be large so we only load it when this page is + // loaded for the first time. This makes opening the options window + // faster and consume less memory if the user never opens the fonts + // dialog. + if (!this.hasShown) { + chrome.send('fetchFontsData'); + this.hasShown = true; + } + }, + + /** + * Handler that is called when the user changes the position of the standard + * font size slider. This allows the UI to show a preview of the change + * before the slider has been released and the associated prefs updated. + * @param {Element} el The slider input element. + * @param {Event} event Change event. + * @private + */ + standardRangeChanged_: function(el, event) { + var size = el.mapPositionToPref(el.value); + var fontSampleEl = $('standard-font-sample'); + this.setUpFontSample_(fontSampleEl, size, fontSampleEl.style.fontFamily, + true); + + fontSampleEl = $('serif-font-sample'); + this.setUpFontSample_(fontSampleEl, size, fontSampleEl.style.fontFamily, + true); + + fontSampleEl = $('sans-serif-font-sample'); + this.setUpFontSample_(fontSampleEl, size, fontSampleEl.style.fontFamily, + true); + + fontSampleEl = $('fixed-font-sample'); + this.setUpFontSample_(fontSampleEl, + size - OptionsPage.SIZE_DIFFERENCE_FIXED_STANDARD, + fontSampleEl.style.fontFamily, false); + }, + + /** + * Sets the 'default_fixed_font_size' preference when the user changes the + * standard font size. + * @param {Event} event Change event. + * @private + */ + standardFontSizeChanged_: function(event) { + var size = this.mapPositionToPref(this.value); + Preferences.setIntegerPref( + 'webkit.webprefs.default_fixed_font_size', + size - OptionsPage.SIZE_DIFFERENCE_FIXED_STANDARD, true); + return false; + }, + + /** + * Handler that is called when the user changes the position of the minimum + * font size slider. This allows the UI to show a preview of the change + * before the slider has been released and the associated prefs updated. + * @param {Element} el The slider input element. + * @param {Event} event Change event. + * @private + */ + minimumRangeChanged_: function(el, event) { + var size = el.mapPositionToPref(el.value); + var fontSampleEl = $('minimum-font-sample'); + this.setUpFontSample_(fontSampleEl, size, fontSampleEl.style.fontFamily, + true); + }, + + /** + * Sets the 'minimum_logical_font_size' preference when the user changes the + * minimum font size. + * @param {Event} event Change event. + * @private + */ + minimumFontSizeChanged_: function(event) { + var size = this.mapPositionToPref(this.value); + Preferences.setIntegerPref( + 'webkit.webprefs.minimum_logical_font_size', size, true); + return false; + }, + + /** + * Sets the text, font size and font family of the sample text. + * @param {Element} el The div containing the sample text. + * @param {number} size The font size of the sample text. + * @param {string} font The font family of the sample text. + * @param {bool} showSize True if the font size should appear in the sample. + * @private + */ + setUpFontSample_: function(el, size, font, showSize) { + var prefix = showSize ? (size + ': ') : ''; + el.textContent = prefix + + loadTimeData.getString('fontSettingsLoremIpsum'); + el.style.fontSize = size + 'px'; + if (font) + el.style.fontFamily = font; + }, + + /** + * Populates a select list and selects the specified item. + * @param {Element} element The select element to populate. + * @param {Array} items The array of items from which to populate. + * @param {string} selectedValue The selected item. + * @private + */ + populateSelect_: function(element, items, selectedValue) { + // Remove any existing content. + element.textContent = ''; + + // Insert new child nodes into select element. + var value, text, selected, option; + for (var i = 0; i < items.length; i++) { + value = items[i][0]; + text = items[i][1]; + dir = items[i][2]; + if (text) { + selected = value == selectedValue; + var option = new Option(text, value, false, selected); + option.dir = dir; + element.appendChild(option); + } else { + element.appendChild(document.createElement('hr')); + } + } + + element.setDisabled('noFontsAvailable', false); + } + }; + + // Chrome callbacks + FontSettings.setFontsData = function(fonts, encodings, selectedValues) { + FontSettings.getInstance().populateSelect_($('standard-font-family'), fonts, + selectedValues[0]); + FontSettings.getInstance().populateSelect_($('serif-font-family'), fonts, + selectedValues[1]); + FontSettings.getInstance().populateSelect_($('sans-serif-font-family'), + fonts, selectedValues[2]); + FontSettings.getInstance().populateSelect_($('fixed-font-family'), fonts, + selectedValues[3]); + FontSettings.getInstance().populateSelect_($('font-encoding'), encodings, + selectedValues[4]); + }; + + FontSettings.setUpStandardFontSample = function(font, size) { + FontSettings.getInstance().setUpFontSample_($('standard-font-sample'), size, + font, true); + }; + + FontSettings.setUpSerifFontSample = function(font, size) { + FontSettings.getInstance().setUpFontSample_($('serif-font-sample'), size, + font, true); + }; + + FontSettings.setUpSansSerifFontSample = function(font, size) { + FontSettings.getInstance().setUpFontSample_($('sans-serif-font-sample'), + size, font, true); + }; + + FontSettings.setUpFixedFontSample = function(font, size) { + FontSettings.getInstance().setUpFontSample_($('fixed-font-sample'), + size, font, false); + }; + + FontSettings.setUpMinimumFontSample = function(size) { + // If size is less than 6, represent it as six in the sample to account + // for the minimum logical font size. + if (size < 6) + size = 6; + FontSettings.getInstance().setUpFontSample_($('minimum-font-sample'), size, + null, true); + }; + + /** + * @param {boolean} available Whether the Advanced Font Settings Extension + * is installed and enabled. + */ + FontSettings.notifyAdvancedFontSettingsAvailability = function(available) { + $('advanced-font-settings-install').hidden = available; + $('advanced-font-settings-options').hidden = !available; + }; + + // Export + return { + FontSettings: FontSettings + }; +}); + diff --git a/chromium/chrome/browser/resources/options/geolocation_options.js b/chromium/chrome/browser/resources/options/geolocation_options.js new file mode 100644 index 00000000000..1215ea9e11f --- /dev/null +++ b/chromium/chrome/browser/resources/options/geolocation_options.js @@ -0,0 +1,34 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + var OptionsPage = options.OptionsPage; + + /** + * GeolocationOptions class + * Handles initialization of the geolocation options. + * @constructor + * @class + */ + function GeolocationOptions() { + OptionsPage.call(this, + 'geolocationOptions', + loadTimeData.getString('geolocationOptionsPageTabTitle'), + 'geolocationCheckbox'); + }; + + cr.addSingletonGetter(GeolocationOptions); + + GeolocationOptions.prototype = { + __proto__: OptionsPage.prototype + }; + + // TODO(robliao): Determine if a full unroll is necessary + // (http://crbug.com/306613). + GeolocationOptions.showGeolocationOption = function() {}; + + return { + GeolocationOptions: GeolocationOptions + }; +}); diff --git a/chromium/chrome/browser/resources/options/handler_options.css b/chromium/chrome/browser/resources/options/handler_options.css new file mode 100644 index 00000000000..116e5e7435f --- /dev/null +++ b/chromium/chrome/browser/resources/options/handler_options.css @@ -0,0 +1,55 @@ +/* Copyright (c) 2012 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ + +.handlers-column-headers { + display: -webkit-box; + font-size: 13px; + font-weight: bold; +} + +.handlers-type-column { + -webkit-margin-end: 10px; + -webkit-margin-start: 14px; + width: 100px; +} + +.handlers-site-column { + max-width: 180px; +} + +.handlers-site-column select { + max-width: 170px; +} + +.handlers-remove-column { + -webkit-box-flex: 1; +} + +.handlers-remove-link { + -webkit-transition: 150ms opacity; + color: #555; + cursor: pointer; + opacity: 0; + padding-left: 14px; + text-decoration: underline; +} + +div > .handlers-remove-column { + opacity: 0; +} + +div:not(.none):hover > .handlers-remove-column { + opacity: 1; +} + +#handlers { + min-height: 250px; +} + +#handler-options list { + border: solid 1px #D9D9D9; + border-radius: 2px; + margin-bottom: 10px; + margin-top: 4px; +} diff --git a/chromium/chrome/browser/resources/options/handler_options.html b/chromium/chrome/browser/resources/options/handler_options.html new file mode 100644 index 00000000000..4f45f018a3a --- /dev/null +++ b/chromium/chrome/browser/resources/options/handler_options.html @@ -0,0 +1,38 @@ +<div id="handler-options" class="page" hidden> + <div class="close-button"></div> + <h1 i18n-content="handlersPage"></h1> + <div class="content-area"> + <h3 i18n-content="handlers_active_heading"></h3> + <div class="handlers-column-headers"> + <div class="handlers-type-column"> + <div i18n-content="handlers_type_column_header"></div> + </div> + <div class="handlers-site-column"> + <div i18n-content="handlers_site_column_header"></div> + </div> + <div class="handlers-remove-column"></div> + </div> + <list id="handlers-list"></list> + + <div id="ignored-handlers-section"> + <h3 i18n-content="handlers_ignored_heading"></h3> + <div class="handlers-column-headers"> + <div class="handlers-type-column"> + <h3 i18n-content="handlers_type_column_header"></h3> + </div> + <div class="handlers-site-column"> + <h3 i18n-content="handlers_site_column_header"></h3> + </div> + <div class="handlers-remove-column"></div> + </div> + <list id="ignored-handlers-list"></list> + </div> + </div> + <div class="action-area"> + <div class="button-strip"> + <button id="handler-options-overlay-confirm" class="default-button" + i18n-content="done"> + </button> + </div> + </div> +</div> diff --git a/chromium/chrome/browser/resources/options/handler_options.js b/chromium/chrome/browser/resources/options/handler_options.js new file mode 100644 index 00000000000..894e13f72c0 --- /dev/null +++ b/chromium/chrome/browser/resources/options/handler_options.js @@ -0,0 +1,80 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + /** @const */ var OptionsPage = options.OptionsPage; + + ///////////////////////////////////////////////////////////////////////////// + // HandlerOptions class: + + /** + * Encapsulated handling of handler options page. + * @constructor + */ + function HandlerOptions() { + this.activeNavTab = null; + OptionsPage.call(this, + 'handlers', + loadTimeData.getString('handlersPageTabTitle'), + 'handler-options'); + } + + cr.addSingletonGetter(HandlerOptions); + + HandlerOptions.prototype = { + __proto__: OptionsPage.prototype, + + /** + * The handlers list. + * @type {DeletableItemList} + * @private + */ + handlersList_: null, + + /** @override */ + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + + this.createHandlersList_(); + + $('handler-options-overlay-confirm').onclick = + OptionsPage.closeOverlay.bind(OptionsPage); + }, + + /** + * Creates, decorates and initializes the handlers list. + * @private + */ + createHandlersList_: function() { + this.handlersList_ = $('handlers-list'); + options.HandlersList.decorate(this.handlersList_); + this.handlersList_.autoExpands = true; + + this.ignoredHandlersList_ = $('ignored-handlers-list'); + options.IgnoredHandlersList.decorate(this.ignoredHandlersList_); + this.ignoredHandlersList_.autoExpands = true; + }, + }; + + /** + * Sets the list of handlers shown by the view. + * @param {Array} Handlers to be shown in the view. + */ + HandlerOptions.setHandlers = function(handlers) { + $('handlers-list').setHandlers(handlers); + }; + + /** + * Sets the list of ignored handlers shown by the view. + * @param {Array} Handlers to be shown in the view. + */ + HandlerOptions.setIgnoredHandlers = function(handlers) { + $('ignored-handlers-section').hidden = handlers.length == 0; + $('ignored-handlers-list').setHandlers(handlers); + }; + + return { + HandlerOptions: HandlerOptions + }; +}); diff --git a/chromium/chrome/browser/resources/options/handler_options_list.js b/chromium/chrome/browser/resources/options/handler_options_list.js new file mode 100644 index 00000000000..5572a55445f --- /dev/null +++ b/chromium/chrome/browser/resources/options/handler_options_list.js @@ -0,0 +1,227 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + /** @const */ var ArrayDataModel = cr.ui.ArrayDataModel; + /** @const */ var List = cr.ui.List; + /** @const */ var ListItem = cr.ui.ListItem; + /** @const */ var HandlerOptions = options.HandlerOptions; + /** @const */ var DeletableItem = options.DeletableItem; + /** @const */ var DeletableItemList = options.DeletableItemList; + + /** + * Creates a new ignored protocol / content handler list item. + * + * Accepts values in the form + * ['mailto', 'http://www.thesite.com/%s', 'The title of the protocol'], + * @param {Object} entry A dictionary describing the handlers for a given + * protocol. + * @constructor + * @extends {cr.ui.DeletableItemList} + */ + function IgnoredHandlersListItem(entry) { + var el = cr.doc.createElement('div'); + el.dataItem = entry; + el.__proto__ = IgnoredHandlersListItem.prototype; + el.decorate(); + return el; + } + + IgnoredHandlersListItem.prototype = { + __proto__: DeletableItem.prototype, + + /** @override */ + decorate: function() { + DeletableItem.prototype.decorate.call(this); + + // Protocol. + var protocolElement = document.createElement('div'); + protocolElement.textContent = this.dataItem[0]; + protocolElement.className = 'handlers-type-column'; + this.contentElement_.appendChild(protocolElement); + + // Site title. + var titleElement = document.createElement('div'); + titleElement.textContent = this.dataItem[2]; + titleElement.className = 'handlers-site-column'; + titleElement.title = this.dataItem[1]; + this.contentElement_.appendChild(titleElement); + }, + }; + + + var IgnoredHandlersList = cr.ui.define('list'); + + IgnoredHandlersList.prototype = { + __proto__: DeletableItemList.prototype, + + createItem: function(entry) { + return new IgnoredHandlersListItem(entry); + }, + + deleteItemAtIndex: function(index) { + chrome.send('removeIgnoredHandler', [this.dataModel.item(index)]); + }, + + /** + * The length of the list. + */ + get length() { + return this.dataModel.length; + }, + + /** + * Set the protocol handlers displayed by this list. See + * IgnoredHandlersListItem for an example of the format the list should + * take. + * + * @param {Object} list A list of ignored protocol handlers. + */ + setHandlers: function(list) { + this.dataModel = new ArrayDataModel(list); + }, + }; + + + + /** + * Creates a new protocol / content handler list item. + * + * Accepts values in the form + * { protocol: 'mailto', + * handlers: [ + * ['mailto', 'http://www.thesite.com/%s', 'The title of the protocol'], + * ..., + * ], + * } + * @param {Object} entry A dictionary describing the handlers for a given + * protocol. + * @constructor + * @extends {cr.ui.ListItem} + */ + function HandlerListItem(entry) { + var el = cr.doc.createElement('div'); + el.dataItem = entry; + el.__proto__ = HandlerListItem.prototype; + el.decorate(); + return el; + } + + HandlerListItem.prototype = { + __proto__: ListItem.prototype, + + buildWidget_: function(data, delegate) { + // Protocol. + var protocolElement = document.createElement('div'); + protocolElement.textContent = data.protocol; + protocolElement.className = 'handlers-type-column'; + this.appendChild(protocolElement); + + // Handler selection. + var handlerElement = document.createElement('div'); + var selectElement = document.createElement('select'); + var defaultOptionElement = document.createElement('option'); + defaultOptionElement.selected = data.default_handler == -1; + defaultOptionElement.textContent = + loadTimeData.getString('handlers_none_handler'); + defaultOptionElement.value = -1; + selectElement.appendChild(defaultOptionElement); + + for (var i = 0; i < data.handlers.length; ++i) { + var optionElement = document.createElement('option'); + optionElement.selected = i == data.default_handler; + optionElement.textContent = data.handlers[i][2]; + optionElement.value = i; + selectElement.appendChild(optionElement); + } + + selectElement.addEventListener('change', function(e) { + var index = e.target.value; + if (index == -1) { + this.classList.add('none'); + delegate.clearDefault(data.protocol); + } else { + handlerElement.classList.remove('none'); + delegate.setDefault(data.handlers[index]); + } + }); + handlerElement.appendChild(selectElement); + handlerElement.className = 'handlers-site-column'; + if (data.default_handler == -1) + this.classList.add('none'); + this.appendChild(handlerElement); + + // Remove link. + var removeElement = document.createElement('div'); + removeElement.textContent = + loadTimeData.getString('handlers_remove_link'); + removeElement.addEventListener('click', function(e) { + var value = selectElement ? selectElement.value : 0; + delegate.removeHandler(value, data.handlers[value]); + }); + removeElement.className = 'handlers-remove-column handlers-remove-link'; + this.appendChild(removeElement); + }, + + /** @override */ + decorate: function() { + ListItem.prototype.decorate.call(this); + + var self = this; + var delegate = { + removeHandler: function(index, handler) { + chrome.send('removeHandler', [handler]); + }, + setDefault: function(handler) { + chrome.send('setDefault', [handler]); + }, + clearDefault: function(protocol) { + chrome.send('clearDefault', [protocol]); + }, + }; + + this.buildWidget_(this.dataItem, delegate); + }, + }; + + /** + * Create a new passwords list. + * @constructor + * @extends {cr.ui.List} + */ + var HandlersList = cr.ui.define('list'); + + HandlersList.prototype = { + __proto__: List.prototype, + + /** @override */ + createItem: function(entry) { + return new HandlerListItem(entry); + }, + + /** + * The length of the list. + */ + get length() { + return this.dataModel.length; + }, + + /** + * Set the protocol handlers displayed by this list. + * See HandlerListItem for an example of the format the list should take. + * + * @param {Object} list A list of protocols with their registered handlers. + */ + setHandlers: function(list) { + this.dataModel = new ArrayDataModel(list); + }, + }; + + return { + IgnoredHandlersListItem: IgnoredHandlersListItem, + IgnoredHandlersList: IgnoredHandlersList, + HandlerListItem: HandlerListItem, + HandlersList: HandlersList, + }; +}); diff --git a/chromium/chrome/browser/resources/options/home_page_overlay.css b/chromium/chrome/browser/resources/options/home_page_overlay.css new file mode 100644 index 00000000000..853b256874c --- /dev/null +++ b/chromium/chrome/browser/resources/options/home_page_overlay.css @@ -0,0 +1,16 @@ +/* Copyright (c) 2012 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ + +#homepage-url-field { + bottom: 1px; + display: block; + margin-left: 10px; + position: relative; +} + +/* -webkit-margin-start doesn't work as you'd hope on .weakrtl elements. */ +html[dir='rtl'] #homepage-url-field { + margin-left: 0; + margin-right: 10px; +} diff --git a/chromium/chrome/browser/resources/options/home_page_overlay.html b/chromium/chrome/browser/resources/options/home_page_overlay.html new file mode 100644 index 00000000000..7fc78398d50 --- /dev/null +++ b/chromium/chrome/browser/resources/options/home_page_overlay.html @@ -0,0 +1,53 @@ +<div id="home-page-overlay" class="page" role="dialog" hidden> + <div class="close-button"></div> + <h1 i18n-content="homePageOverlay"></h1> + <div class="content-area"> + <div class="radio"> + <span class="controlled-setting-with-label"> + <input id="homepage-use-ntp" type="radio" name="homepage" + pref="homepage_is_newtabpage" value="true" + metric="Options_Homepage_IsNewTabPage" dialog-pref> + <span> + <label for="homepage-use-ntp" i18n-content="homePageUseNewTab"> + </label> + <span class="controlled-setting-indicator" + pref="homepage_is_newtabpage" value="true" dialog-pref> + </span> + </span> + </span> + </div> + <div class="radio"> + <span class="controlled-setting-with-label"> + <input id="homepage-use-url" type="radio" name="homepage" + pref="homepage_is_newtabpage" value="false" + metric="Options_Homepage_IsNewTabPage" dialog-pref> + <span> + <label id="homepage-use-url-label" i18n-content="homePageUseURL" + for="homepage-use-url"> + </label> + <span class="controlled-setting-indicator" + pref="homepage_is_newtabpage" value="false" dialog-pref> + </span> + <input id="homepage-url-field" type="url" data-type="url" + class="weakrtl favicon-cell stretch" pref="homepage" + aria-labelledby="homepage-use-url-label" + dialog-pref> + </input> + <span id="homepage-url-field-indicator" + class="controlled-setting-indicator" pref="homepage" + dialog-pref> + </span> + </span> + </span> + </div> + </div> + <div class="action-area"> + <div class="button-strip"> + <button id="home-page-cancel" type="reset" i18n-content="cancel"> + </button> + <button id="home-page-confirm" class="default-button" + i18n-content="ok"> + </button> + </div> + </div> +</div> diff --git a/chromium/chrome/browser/resources/options/home_page_overlay.js b/chromium/chrome/browser/resources/options/home_page_overlay.js new file mode 100644 index 00000000000..6649f930e2e --- /dev/null +++ b/chromium/chrome/browser/resources/options/home_page_overlay.js @@ -0,0 +1,155 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + /** @const */ var OptionsPage = options.OptionsPage; + /** @const */ var SettingsDialog = options.SettingsDialog; + + /** + * HomePageOverlay class + * Dialog that allows users to set the home page. + * @extends {SettingsDialog} + */ + function HomePageOverlay() { + SettingsDialog.call(this, 'homePageOverlay', + loadTimeData.getString('homePageOverlayTabTitle'), + 'home-page-overlay', + $('home-page-confirm'), $('home-page-cancel')); + } + + cr.addSingletonGetter(HomePageOverlay); + + HomePageOverlay.prototype = { + __proto__: SettingsDialog.prototype, + + /** + * An autocomplete list that can be attached to the home page URL field. + * @type {cr.ui.AutocompleteList} + * @private + */ + autocompleteList_: null, + + /** + * Initialize the page. + */ + initializePage: function() { + // Call base class implementation to start preference initialization. + SettingsDialog.prototype.initializePage.call(this); + + var self = this; + options.Preferences.getInstance().addEventListener( + 'homepage_is_newtabpage', + this.handleHomepageIsNTPPrefChange.bind(this)); + + var urlField = $('homepage-url-field'); + urlField.addEventListener('keydown', function(event) { + // Don't auto-submit when the user selects something from the + // auto-complete list. + if (event.keyIdentifier == 'Enter' && !self.autocompleteList_.hidden) + event.stopPropagation(); + }); + urlField.addEventListener('change', this.updateFavicon_.bind(this)); + + var suggestionList = new cr.ui.AutocompleteList(); + suggestionList.autoExpands = true; + suggestionList.requestSuggestions = + this.requestAutocompleteSuggestions_.bind(this); + $('home-page-overlay').appendChild(suggestionList); + this.autocompleteList_ = suggestionList; + + urlField.addEventListener('focus', function(event) { + self.autocompleteList_.attachToInput(urlField); + }); + urlField.addEventListener('blur', function(event) { + self.autocompleteList_.detach(); + }); + }, + + /** @override */ + didShowPage: function() { + this.updateFavicon_(); + }, + + /** + * Updates the state of the homepage text input and its controlled setting + * indicator when the |homepage_is_newtabpage| pref changes. The input is + * enabled only if the homepage is not the NTP. The indicator is always + * enabled but treats the input's value as read-only if the homepage is the + * NTP. + * @param {Event} Pref change event. + */ + handleHomepageIsNTPPrefChange: function(event) { + var urlField = $('homepage-url-field'); + var urlFieldIndicator = $('homepage-url-field-indicator'); + urlField.setDisabled('homepage-is-ntp', event.value.value); + urlFieldIndicator.readOnly = event.value.value; + }, + + /** + * Updates the background of the url field to show the favicon for the + * URL that is currently typed in. + * @private + */ + updateFavicon_: function() { + var urlField = $('homepage-url-field'); + urlField.style.backgroundImage = getFaviconImageSet(urlField.value); + }, + + /** + * Sends an asynchronous request for new autocompletion suggestions for the + * the given query. When new suggestions are available, the C++ handler will + * call updateAutocompleteSuggestions_. + * @param {string} query List of autocomplete suggestions. + * @private + */ + requestAutocompleteSuggestions_: function(query) { + chrome.send('requestAutocompleteSuggestionsForHomePage', [query]); + }, + + /** + * Updates the autocomplete suggestion list with the given entries. + * @param {Array} pages List of autocomplete suggestions. + * @private + */ + updateAutocompleteSuggestions_: function(suggestions) { + var list = this.autocompleteList_; + // If the trigger for this update was a value being selected from the + // current list, do nothing. + if (list.targetInput && list.selectedItem && + list.selectedItem.url == list.targetInput.value) { + return; + } + list.suggestions = suggestions; + }, + + /** + * Sets the 'show home button' and 'home page is new tab page' preferences. + * (The home page url preference is set automatically by the SettingsDialog + * code.) + */ + handleConfirm: function() { + // Strip whitespace. + var urlField = $('homepage-url-field'); + var homePageValue = urlField.value.replace(/\s*/g, ''); + urlField.value = homePageValue; + + // Don't save an empty URL for the home page. If the user left the field + // empty, switch to the New Tab page. + if (!homePageValue) + $('homepage-use-ntp').checked = true; + + SettingsDialog.prototype.handleConfirm.call(this); + }, + }; + + HomePageOverlay.updateAutocompleteSuggestions = function() { + var instance = HomePageOverlay.getInstance(); + instance.updateAutocompleteSuggestions_.apply(instance, arguments); + }; + + // Export + return { + HomePageOverlay: HomePageOverlay + }; +}); diff --git a/chromium/chrome/browser/resources/options/import_data_overlay.css b/chromium/chrome/browser/resources/options/import_data_overlay.css new file mode 100644 index 00000000000..d7c076d3ed8 --- /dev/null +++ b/chromium/chrome/browser/resources/options/import_data_overlay.css @@ -0,0 +1,30 @@ +/* Copyright (c) 2012 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ + +#import-data-overlay { + width: 400px; +} + +#import-from-div { + margin-bottom: 20px; +} + +#import-checkboxes > div:not(:first-child) { + -webkit-padding-start: 8px; +} + +#import-throbber { + margin: 4px 10px; + vertical-align: middle; + visibility: hidden; +} + +#import-success-header { + font-size: 1.2em; +} + +#import-success-image { + margin: 20px; + text-align: center; +} diff --git a/chromium/chrome/browser/resources/options/import_data_overlay.html b/chromium/chrome/browser/resources/options/import_data_overlay.html new file mode 100644 index 00000000000..57215089b87 --- /dev/null +++ b/chromium/chrome/browser/resources/options/import_data_overlay.html @@ -0,0 +1,122 @@ +<div id="import-data-overlay" class="page" hidden> + <div class="close-button"></div> + <h1 i18n-content="importDataOverlay"></h1> + <div class="content-area"> + <div class="import-data-configure"> + <div id="import-from-div"> + <span i18n-content="importFromLabel"></span> + <select id="import-browsers"> + <option i18n-content="importLoading"></option> + </select> + </div> + <div id="import-checkboxes"> + <div i18n-content="importDescription"></div> + <div class="checkbox"> + <span id="import-history-with-label" + class="controlled-setting-with-label"> + <input id="import-history" type="checkbox" pref="import_history"> + <span> + <label for="import-history" i18n-content="importHistory"></label> + <span class="controlled-setting-indicator" pref="import_history"> + </span> + </span> + </span> + </div> + <div class="checkbox"> + <span id="import-favorites-with-label" + class="controlled-setting-with-label"> + <input id="import-favorites" type="checkbox" + pref="import_bookmarks"> + <span> + <label for="import-favorites" i18n-content="importFavorites"> + </label> + <span class="controlled-setting-indicator" + pref="import_bookmarks"> + </span> + </span> + </span> + </div> + <div class="checkbox"> + <span id="import-passwords-with-label" + class="controlled-setting-with-label"> + <input id="import-passwords" type="checkbox" + pref="import_saved_passwords"> + <span> + <label for="import-passwords" i18n-content="importPasswords"> + </label> + <span class="controlled-setting-indicator" + pref="import_saved_passwords"> + </span> + </span> + </span> + </div> + <div class="checkbox"> + <span id="import-search-with-label" + class="controlled-setting-with-label"> + <input id="import-search" type="checkbox" + pref="import_search_engine"> + <span> + <label for="import-search" i18n-content="importSearch"></label> + <span class="controlled-setting-indicator" + pref="import_search_engine"> + </span> + </span> + </span> + </div> + </div> + </div> + <div class="import-data-success" hidden> + <div id="import-success-header"> + <strong i18n-content="importSucceeded"></strong> + </div> + <div id="import-success-image"> + <img src="../../../../ui/webui/resources/images/success.png"> + </div> + <div id="import-find-your-bookmarks"> + <span i18n-content="findYourImportedBookmarks"></span> + <div class="checkbox"> + <span class="controlled-setting-with-label"> + <input id="import-data-show-bookmarks-bar" + pref="bookmark_bar.show_on_all_tabs" + metric="Options_ShowBookmarksBar" type="checkbox"> + <span> + <label for="import-data-show-bookmarks-bar" + i18n-content="toolbarShowBookmarksBar"> + </label> + <span class="controlled-setting-indicator" + pref="bookmark_bar.show_on_all_tabs"> + </span> + </span> + </span> + </div> + </div> + </div> + </div> + <div class="action-area"> + <div class="import-data-configure"> + <div class="action-area-right"> + <div id="import-throbber" class="throbber"></div> + <div class="button-strip"> + <button id="import-data-cancel" i18n-content="cancel"></button> + <button id="import-choose-file" i18n-content="importChooseFile"> + </button> + <button id="import-data-commit" class="default-button" + i18n-content="importCommit"> + </button> + </div> + </div> + </div> + <div class="import-data-success" hidden> + <div class="action-area-right"> + <div class="button-strip"> + <button id="import-data-confirm" class="default-button" + i18n-content="done"> + </button> + </div> + </div> + </div> + </div> + <div id="mac-password-keychain" class="gray-bottom-bar"> + <span i18n-content="macPasswordKeychain"></span> + </div> +</div> diff --git a/chromium/chrome/browser/resources/options/import_data_overlay.js b/chromium/chrome/browser/resources/options/import_data_overlay.js new file mode 100644 index 00000000000..16b8cca6ff2 --- /dev/null +++ b/chromium/chrome/browser/resources/options/import_data_overlay.js @@ -0,0 +1,277 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + var OptionsPage = options.OptionsPage; + + /** + * ImportDataOverlay class + * Encapsulated handling of the 'Import Data' overlay page. + * @class + */ + function ImportDataOverlay() { + OptionsPage.call(this, + 'importData', + loadTimeData.getString('importDataOverlayTabTitle'), + 'import-data-overlay'); + } + + cr.addSingletonGetter(ImportDataOverlay); + + ImportDataOverlay.prototype = { + // Inherit from OptionsPage. + __proto__: OptionsPage.prototype, + + /** + * Initialize the page. + */ + initializePage: function() { + // Call base class implementation to start preference initialization. + OptionsPage.prototype.initializePage.call(this); + + var self = this; + var checkboxes = + document.querySelectorAll('#import-checkboxes input[type=checkbox]'); + for (var i = 0; i < checkboxes.length; i++) { + checkboxes[i].onchange = function() { + self.validateCommitButton_(); + }; + } + + $('import-browsers').onchange = function() { + self.updateCheckboxes_(); + self.validateCommitButton_(); + self.updateBottomBar_(); + }; + + $('import-data-commit').onclick = function() { + chrome.send('importData', [ + String($('import-browsers').selectedIndex), + String($('import-history').checked), + String($('import-favorites').checked), + String($('import-passwords').checked), + String($('import-search').checked)]); + }; + + $('import-data-cancel').onclick = function() { + ImportDataOverlay.dismiss(); + }; + + $('import-choose-file').onclick = function() { + chrome.send('chooseBookmarksFile'); + }; + + $('import-data-show-bookmarks-bar').onchange = function() { + // Note: The callback 'toggleShowBookmarksBar' is handled within the + // browser options handler -- rather than the import data handler -- + // as the implementation is shared by several clients. + chrome.send('toggleShowBookmarksBar'); + } + + $('import-data-confirm').onclick = function() { + ImportDataOverlay.dismiss(); + }; + + // Form controls are disabled until the profile list has been loaded. + self.setAllControlsEnabled_(false); + }, + + /** + * Sets the enabled and checked state of the commit button. + * @private + */ + validateCommitButton_: function() { + var somethingToImport = + $('import-history').checked || $('import-favorites').checked || + $('import-passwords').checked || $('import-search').checked; + $('import-data-commit').disabled = !somethingToImport; + $('import-choose-file').disabled = !$('import-favorites').checked; + }, + + /** + * Sets the enabled state of all the checkboxes and the commit button. + * @private + */ + setAllControlsEnabled_: function(enabled) { + var checkboxes = + document.querySelectorAll('#import-checkboxes input[type=checkbox]'); + for (var i = 0; i < checkboxes.length; i++) + this.setUpCheckboxState_(checkboxes[i], enabled); + $('import-data-commit').disabled = !enabled; + $('import-choose-file').hidden = !enabled; + $('mac-password-keychain').hidden = !enabled; + }, + + /** + * Sets the enabled state of a checkbox element. + * @param {Object} checkbox A checkbox element. + * @param {boolean} enabled The enabled state of the checkbox. If false, + * the checkbox is disabled. If true, the checkbox is enabled. + * @private + */ + setUpCheckboxState_: function(checkbox, enabled) { + checkbox.setDisabled('noProfileData', !enabled); + }, + + /** + * Update the enabled and visible states of all the checkboxes. + * @private + */ + updateCheckboxes_: function() { + var index = $('import-browsers').selectedIndex; + var bookmarksFileSelected = index == this.browserProfiles.length - 1; + $('import-choose-file').hidden = !bookmarksFileSelected; + $('import-data-commit').hidden = bookmarksFileSelected; + + var browserProfile; + if (this.browserProfiles.length > index) + browserProfile = this.browserProfiles[index]; + var importOptions = ['history', 'favorites', 'passwords', 'search']; + for (var i = 0; i < importOptions.length; i++) { + var checkbox = $('import-' + importOptions[i]); + var enable = browserProfile && browserProfile[importOptions[i]]; + this.setUpCheckboxState_(checkbox, enable); + var checkboxWithLabel = $('import-' + importOptions[i] + '-with-label'); + checkboxWithLabel.style.display = enable ? '' : 'none'; + } + }, + + /** + * Show or hide gray message at the bottom. + * @private + */ + updateBottomBar_: function() { + var index = $('import-browsers').selectedIndex; + var browserProfile; + if (this.browserProfiles.length > index) + browserProfile = this.browserProfiles[index]; + var enable = browserProfile && browserProfile['show_bottom_bar']; + $('mac-password-keychain').hidden = !enable; + }, + + /** + * Update the supported browsers popup with given entries. + * @param {array} browsers List of supported browsers name. + * @private + */ + updateSupportedBrowsers_: function(browsers) { + this.browserProfiles = browsers; + var browserSelect = $('import-browsers'); + browserSelect.remove(0); // Remove the 'Loading...' option. + browserSelect.textContent = ''; + var browserCount = browsers.length; + + if (browserCount == 0) { + var option = new Option(loadTimeData.getString('noProfileFound'), 0); + browserSelect.appendChild(option); + + this.setAllControlsEnabled_(false); + } else { + this.setAllControlsEnabled_(true); + for (var i = 0; i < browserCount; i++) { + var browser = browsers[i]; + var option = new Option(browser.name, browser.index); + browserSelect.appendChild(option); + } + + this.updateCheckboxes_(); + this.validateCommitButton_(); + this.updateBottomBar_(); + } + }, + + /** + * Clear import prefs set when user checks/unchecks a checkbox so that each + * checkbox goes back to the default "checked" state (or alternatively, to + * the state set by a recommended policy). + * @private + */ + clearUserPrefs_: function() { + var importPrefs = ['import_history', + 'import_bookmarks', + 'import_saved_passwords', + 'import_search_engine']; + for (var i = 0; i < importPrefs.length; i++) + Preferences.clearPref(importPrefs[i], true); + }, + + /** + * Update the dialog layout to reflect success state. + * @param {boolean} success If true, show success dialog elements. + * @private + */ + updateSuccessState_: function(success) { + var sections = document.querySelectorAll('.import-data-configure'); + for (var i = 0; i < sections.length; i++) + sections[i].hidden = success; + + sections = document.querySelectorAll('.import-data-success'); + for (var i = 0; i < sections.length; i++) + sections[i].hidden = !success; + }, + }; + + ImportDataOverlay.clearUserPrefs = function() { + ImportDataOverlay.getInstance().clearUserPrefs_(); + }; + + /** + * Update the supported browsers popup with given entries. + * @param {array} list of supported browsers name. + */ + ImportDataOverlay.updateSupportedBrowsers = function(browsers) { + ImportDataOverlay.getInstance().updateSupportedBrowsers_(browsers); + }; + + /** + * Update the UI to reflect whether an import operation is in progress. + * @param {boolean} importing True if an import operation is in progress. + */ + ImportDataOverlay.setImportingState = function(importing) { + var checkboxes = + document.querySelectorAll('#import-checkboxes input[type=checkbox]'); + for (var i = 0; i < checkboxes.length; i++) + checkboxes[i].setDisabled('Importing', importing); + if (!importing) + ImportDataOverlay.getInstance().updateCheckboxes_(); + $('import-browsers').disabled = importing; + $('import-throbber').style.visibility = importing ? 'visible' : 'hidden'; + ImportDataOverlay.getInstance().validateCommitButton_(); + }; + + /** + * Remove the import overlay from display. + */ + ImportDataOverlay.dismiss = function() { + ImportDataOverlay.clearUserPrefs(); + OptionsPage.closeOverlay(); + }; + + /** + * Show a message confirming the success of the import operation. + */ + ImportDataOverlay.confirmSuccess = function() { + var showBookmarksMessage = $('import-favorites').checked; + ImportDataOverlay.setImportingState(false); + $('import-find-your-bookmarks').hidden = !showBookmarksMessage; + ImportDataOverlay.getInstance().updateSuccessState_(true); + }; + + /** + * Show the import data overlay. + */ + ImportDataOverlay.show = function() { + // Make sure that any previous import success message is hidden, and + // we're showing the UI to import further data. + ImportDataOverlay.getInstance().updateSuccessState_(false); + ImportDataOverlay.getInstance().validateCommitButton_(); + + OptionsPage.navigateToPage('importData'); + }; + + // Export + return { + ImportDataOverlay: ImportDataOverlay + }; +}); diff --git a/chromium/chrome/browser/resources/options/inline_editable_list.js b/chromium/chrome/browser/resources/options/inline_editable_list.js new file mode 100644 index 00000000000..7a502fa46ba --- /dev/null +++ b/chromium/chrome/browser/resources/options/inline_editable_list.js @@ -0,0 +1,459 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + /** @const */ var DeletableItem = options.DeletableItem; + /** @const */ var DeletableItemList = options.DeletableItemList; + + /** + * Creates a new list item with support for inline editing. + * @constructor + * @extends {options.DeletableListItem} + */ + function InlineEditableItem() { + var el = cr.doc.createElement('div'); + InlineEditableItem.decorate(el); + return el; + } + + /** + * Decorates an element as a inline-editable list item. Note that this is + * a subclass of DeletableItem. + * @param {!HTMLElement} el The element to decorate. + */ + InlineEditableItem.decorate = function(el) { + el.__proto__ = InlineEditableItem.prototype; + el.decorate(); + }; + + InlineEditableItem.prototype = { + __proto__: DeletableItem.prototype, + + /** + * Whether or not this item can be edited. + * @type {boolean} + * @private + */ + editable_: true, + + /** + * Whether or not this is a placeholder for adding a new item. + * @type {boolean} + * @private + */ + isPlaceholder_: false, + + /** + * Fields associated with edit mode. + * @type {array} + * @private + */ + editFields_: null, + + /** + * Whether or not the current edit should be considered cancelled, rather + * than committed, when editing ends. + * @type {boolean} + * @private + */ + editCancelled_: true, + + /** + * The editable item corresponding to the last click, if any. Used to decide + * initial focus when entering edit mode. + * @type {HTMLElement} + * @private + */ + editClickTarget_: null, + + /** @override */ + decorate: function() { + DeletableItem.prototype.decorate.call(this); + + this.editFields_ = []; + this.addEventListener('mousedown', this.handleMouseDown_); + this.addEventListener('keydown', this.handleKeyDown_); + this.addEventListener('leadChange', this.handleLeadChange_); + }, + + /** @override */ + selectionChanged: function() { + this.updateEditState(); + }, + + /** + * Called when this element gains or loses 'lead' status. Updates editing + * mode accordingly. + * @private + */ + handleLeadChange_: function() { + this.updateEditState(); + }, + + /** + * Updates the edit state based on the current selected and lead states. + */ + updateEditState: function() { + if (this.editable) + this.editing = this.selected && this.lead; + }, + + /** + * Whether the user is currently editing the list item. + * @type {boolean} + */ + get editing() { + return this.hasAttribute('editing'); + }, + set editing(editing) { + if (this.editing == editing) + return; + + if (editing) + this.setAttribute('editing', ''); + else + this.removeAttribute('editing'); + + if (editing) { + this.editCancelled_ = false; + + cr.dispatchSimpleEvent(this, 'edit', true); + + var focusElement = this.editClickTarget_ || this.initialFocusElement; + this.editClickTarget_ = null; + + if (focusElement) { + var self = this; + // We should delay to give focus on |focusElement| if this is called + // in mousedown event handler. If we did give focus immediately, Blink + // would try to focus on an ancestor of the mousedown target element, + // and remove focus from |focusElement|. + if (focusElement.staticVersion && + focusElement.staticVersion.hasAttribute('tabindex')) { + setTimeout(function() { + if (self.editing) { + if (focusElement.disabled) + self.parentNode.focus(); + self.focusAndMaybeSelect_(focusElement); + } + focusElement.staticVersion.removeAttribute('tabindex'); + }, 0); + } else { + this.focusAndMaybeSelect_(focusElement); + } + } + } else { + if (!this.editCancelled_ && this.hasBeenEdited && + this.currentInputIsValid) { + if (this.isPlaceholder) + this.parentNode.focusPlaceholder = true; + + this.updateStaticValues_(); + cr.dispatchSimpleEvent(this, 'commitedit', true); + } else { + this.resetEditableValues_(); + cr.dispatchSimpleEvent(this, 'canceledit', true); + } + } + }, + + /** + * Focus on the specified element, and select the editable text in it + * if possible. + * @param {!Element} control An element to be focused. + * @private + */ + focusAndMaybeSelect_: function(control) { + control.focus(); + if (control.tagName == 'INPUT') + control.select(); + }, + + /** + * Whether the item is editable. + * @type {boolean} + */ + get editable() { + return this.editable_; + }, + set editable(editable) { + this.editable_ = editable; + if (!editable) + this.editing = false; + }, + + /** + * Whether the item is a new item placeholder. + * @type {boolean} + */ + get isPlaceholder() { + return this.isPlaceholder_; + }, + set isPlaceholder(isPlaceholder) { + this.isPlaceholder_ = isPlaceholder; + if (isPlaceholder) + this.deletable = false; + }, + + /** + * The HTML element that should have focus initially when editing starts, + * if a specific element wasn't clicked. + * Defaults to the first <input> element; can be overridden by subclasses if + * a different element should be focused. + * @type {HTMLElement} + */ + get initialFocusElement() { + return this.contentElement.querySelector('input'); + }, + + /** + * Whether the input in currently valid to submit. If this returns false + * when editing would be submitted, either editing will not be ended, + * or it will be cancelled, depending on the context. + * Can be overridden by subclasses to perform input validation. + * @type {boolean} + */ + get currentInputIsValid() { + return true; + }, + + /** + * Returns true if the item has been changed by an edit. + * Can be overridden by subclasses to return false when nothing has changed + * to avoid unnecessary commits. + * @type {boolean} + */ + get hasBeenEdited() { + return true; + }, + + /** + * Returns a div containing an <input>, as well as static text if + * isPlaceholder is not true. + * @param {string} text The text of the cell. + * @return {HTMLElement} The HTML element for the cell. + * @private + */ + createEditableTextCell: function(text) { + var container = this.ownerDocument.createElement('div'); + var textEl; + if (!this.isPlaceholder) { + textEl = this.ownerDocument.createElement('div'); + textEl.className = 'static-text'; + textEl.textContent = text; + textEl.setAttribute('displaymode', 'static'); + container.appendChild(textEl); + } + + var inputEl = this.ownerDocument.createElement('input'); + inputEl.type = 'text'; + inputEl.value = text; + if (!this.isPlaceholder) { + inputEl.setAttribute('displaymode', 'edit'); + } else { + // At this point |this| is not attached to the parent list yet, so give + // a short timeout in order for the attachment to occur. + var self = this; + window.setTimeout(function() { + var list = self.parentNode; + if (list && list.focusPlaceholder) { + list.focusPlaceholder = false; + if (list.shouldFocusPlaceholder()) + inputEl.focus(); + } + }, 50); + } + + // In some cases 'focus' event may arrive before 'input'. + // To make sure revalidation is triggered we postpone 'focus' handling. + var handler = this.handleFocus_.bind(this); + inputEl.addEventListener('focus', function() { + window.setTimeout(function() { + if (inputEl.ownerDocument.activeElement == inputEl) + handler(); + }, 0); + }); + container.appendChild(inputEl); + this.addEditField(inputEl, textEl); + + return container; + }, + + /** + * Register an edit field. + * @param {!Element} control An editable element. It's a form control + * element typically. + * @param {Element} staticElement An element representing non-editable + * state. + */ + addEditField: function(control, staticElement) { + control.staticVersion = staticElement; + this.editFields_.push(control); + }, + + /** + * Resets the editable version of any controls created by createEditable* + * to match the static text. + * @private + */ + resetEditableValues_: function() { + var editFields = this.editFields_; + for (var i = 0; i < editFields.length; i++) { + var staticLabel = editFields[i].staticVersion; + if (!staticLabel && !this.isPlaceholder) + continue; + + if (editFields[i].tagName == 'INPUT') { + editFields[i].value = + this.isPlaceholder ? '' : staticLabel.textContent; + } + // Add more tag types here as new createEditable* methods are added. + + editFields[i].setCustomValidity(''); + } + }, + + /** + * Sets the static version of any controls created by createEditable* + * to match the current value of the editable version. Called on commit so + * that there's no flicker of the old value before the model updates. + * @private + */ + updateStaticValues_: function() { + var editFields = this.editFields_; + for (var i = 0; i < editFields.length; i++) { + var staticLabel = editFields[i].staticVersion; + if (!staticLabel) + continue; + + if (editFields[i].tagName == 'INPUT') + staticLabel.textContent = editFields[i].value; + // Add more tag types here as new createEditable* methods are added. + } + }, + + /** + * Called when a key is pressed. Handles committing and canceling edits. + * @param {Event} e The key down event. + * @private + */ + handleKeyDown_: function(e) { + if (!this.editing) + return; + + var endEdit = false; + var handledKey = true; + switch (e.keyIdentifier) { + case 'U+001B': // Esc + this.editCancelled_ = true; + endEdit = true; + break; + case 'Enter': + if (this.currentInputIsValid) + endEdit = true; + break; + default: + handledKey = false; + } + if (handledKey) { + // Make sure that handled keys aren't passed on and double-handled. + // (e.g., esc shouldn't both cancel an edit and close a subpage) + e.stopPropagation(); + } + if (endEdit) { + // Blurring will trigger the edit to end; see InlineEditableItemList. + this.ownerDocument.activeElement.blur(); + } + }, + + /** + * Called when the list item is clicked. If the click target corresponds to + * an editable item, stores that item to focus when edit mode is started. + * @param {Event} e The mouse down event. + * @private + */ + handleMouseDown_: function(e) { + if (!this.editable || this.editing) + return; + + var clickTarget = e.target; + var editFields = this.editFields_; + for (var i = 0; i < editFields.length; i++) { + if (editFields[i].staticVersion == clickTarget) + clickTarget.tabIndex = 0; + if (editFields[i] == clickTarget || + editFields[i].staticVersion == clickTarget) { + this.editClickTarget_ = editFields[i]; + return; + } + } + }, + }; + + /** + * Takes care of committing changes to inline editable list items when the + * window loses focus. + */ + function handleWindowBlurs() { + window.addEventListener('blur', function(e) { + var itemAncestor = findAncestor(document.activeElement, function(node) { + return node instanceof InlineEditableItem; + }); + if (itemAncestor) + document.activeElement.blur(); + }); + } + handleWindowBlurs(); + + var InlineEditableItemList = cr.ui.define('list'); + + InlineEditableItemList.prototype = { + __proto__: DeletableItemList.prototype, + + /** + * Focuses the input element of the placeholder if true. + * @type {boolean} + */ + focusPlaceholder: false, + + /** @override */ + decorate: function() { + DeletableItemList.prototype.decorate.call(this); + this.setAttribute('inlineeditable', ''); + this.addEventListener('hasElementFocusChange', + this.handleListFocusChange_); + }, + + /** + * Called when the list hierarchy as a whole loses or gains focus; starts + * or ends editing for the lead item if necessary. + * @param {Event} e The change event. + * @private + */ + handleListFocusChange_: function(e) { + var leadItem = this.getListItemByIndex(this.selectionModel.leadIndex); + if (leadItem) { + if (e.newValue) + leadItem.updateEditState(); + else + leadItem.editing = false; + } + }, + + /** + * May be overridden by subclasses to disable focusing the placeholder. + * @return {boolean} True if the placeholder element should be focused on + * edit commit. + */ + shouldFocusPlaceholder: function() { + return true; + }, + }; + + // Export + return { + InlineEditableItem: InlineEditableItem, + InlineEditableItemList: InlineEditableItemList, + }; +}); diff --git a/chromium/chrome/browser/resources/options/language_add_language_overlay.html b/chromium/chrome/browser/resources/options/language_add_language_overlay.html new file mode 100644 index 00000000000..ca8947a6f53 --- /dev/null +++ b/chromium/chrome/browser/resources/options/language_add_language_overlay.html @@ -0,0 +1,16 @@ +<div id="add-language-overlay-page" class="page" role="dialog" hidden> + <div class="close-button"></div> + <h1 i18n-content="addLanguageTitle"></h1> + <div class="content-area"> + <label for="add-language-overlay-language-list" + i18n-content="addLanguageSelectLabel"></label> + <select id="add-language-overlay-language-list"></select> + </div> + <div class="action-area button-strip"> + <button id="add-language-overlay-cancel-button" i18n-content="cancel"> + </button> + <button id="add-language-overlay-ok-button" class="default-button" + i18n-content="ok"> + </button> + </div> +</div> diff --git a/chromium/chrome/browser/resources/options/language_add_language_overlay.js b/chromium/chrome/browser/resources/options/language_add_language_overlay.js new file mode 100644 index 00000000000..f91d170922c --- /dev/null +++ b/chromium/chrome/browser/resources/options/language_add_language_overlay.js @@ -0,0 +1,61 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/////////////////////////////////////////////////////////////////////////////// +// AddLanguageOverlay class: + +cr.define('options', function() { + /** @const */ var OptionsPage = options.OptionsPage; + + /** + * Encapsulated handling of ChromeOS add language overlay page. + * @constructor + */ + function AddLanguageOverlay() { + OptionsPage.call(this, 'addLanguage', + loadTimeData.getString('addButton'), + 'add-language-overlay-page'); + } + + cr.addSingletonGetter(AddLanguageOverlay); + + AddLanguageOverlay.prototype = { + // Inherit AddLanguageOverlay from OptionsPage. + __proto__: OptionsPage.prototype, + + /** + * Initializes AddLanguageOverlay page. + * Calls base class implementation to starts preference initialization. + */ + initializePage: function() { + // Call base class implementation to starts preference initialization. + OptionsPage.prototype.initializePage.call(this); + + // Set up the cancel button. + $('add-language-overlay-cancel-button').onclick = function(e) { + OptionsPage.closeOverlay(); + }; + + // Create the language list with which users can add a language. + var addLanguageList = $('add-language-overlay-language-list'); + var languageListData = loadTimeData.getValue('languageList'); + for (var i = 0; i < languageListData.length; i++) { + var language = languageListData[i]; + var displayText = language.displayName; + // If the native name is different, add it. + if (language.displayName != language.nativeDisplayName) + displayText += ' - ' + language.nativeDisplayName; + + var option = cr.doc.createElement('option'); + option.value = language.code; + option.textContent = displayText; + addLanguageList.appendChild(option); + } + }, + }; + + return { + AddLanguageOverlay: AddLanguageOverlay + }; +}); diff --git a/chromium/chrome/browser/resources/options/language_dictionary_overlay.css b/chromium/chrome/browser/resources/options/language_dictionary_overlay.css new file mode 100644 index 00000000000..b8400479207 --- /dev/null +++ b/chromium/chrome/browser/resources/options/language_dictionary_overlay.css @@ -0,0 +1,47 @@ +/* Copyright (c) 2012 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ + +#language-dictionary-overlay-no-matches { + -webkit-margin-after: 0; + -webkit-margin-before: 0; + line-height: 2.5em; + padding: 0 3px; +} + +#language-dictionary-overlay-page h1 { + margin-right: 16em; + padding-top: 25px; +} + +html[dir=rtl] #language-dictionary-overlay-page h1 { + margin-left: 16em; + margin-right: auto; +} + +#language-dictionary-overlay-search-field { + position: absolute; + right: 32px; + top: 21px; +} + +html[dir=rtl] #language-dictionary-overlay-search-field { + left: 32px; + right: auto; +} + +#language-dictionary-overlay-word-list { + height: 20em; +} + +#language-dictionary-overlay-word-list.no-search-matches { + height: 17.5em; +} + +#language-dictionary-overlay-word-list > * { + height: 2.5em; +} + +.language-dictionary-overlay-word-list-item { + width: 30em; +} diff --git a/chromium/chrome/browser/resources/options/language_dictionary_overlay.html b/chromium/chrome/browser/resources/options/language_dictionary_overlay.html new file mode 100644 index 00000000000..34b64055c2e --- /dev/null +++ b/chromium/chrome/browser/resources/options/language_dictionary_overlay.html @@ -0,0 +1,20 @@ +<div id="language-dictionary-overlay-page" class="page" hidden> + <div class="close-button"></div> + <h1 i18n-content="languageDictionaryOverlayTitle"></h1> + <input id="language-dictionary-overlay-search-field" type="search" + i18n-values="placeholder:languageDictionaryOverlaySearchPlaceholder; + aria-label:languageDictionaryOverlaySearchPlaceholder" + incremental> + <div class="content-area"> + <div class="settings-list"> + <p id="language-dictionary-overlay-no-matches" + i18n-content="languageDictionaryOverlayNoMatches" hidden></p> + <list id="language-dictionary-overlay-word-list"></list> + </div> + </div> + <div class="action-area button-strip"> + <button id="language-dictionary-overlay-done-button" class="default-button" + i18n-content="done"> + </button> + </div> +</div> diff --git a/chromium/chrome/browser/resources/options/language_dictionary_overlay.js b/chromium/chrome/browser/resources/options/language_dictionary_overlay.js new file mode 100644 index 00000000000..63152995569 --- /dev/null +++ b/chromium/chrome/browser/resources/options/language_dictionary_overlay.js @@ -0,0 +1,118 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + /** @const */ var DictionaryWordsList = + options.dictionary_words.DictionaryWordsList; + /** @const */ var OptionsPage = options.OptionsPage; + + /** + * Adding and removing words in custom spelling dictionary. + * @constructor + * @extends {options.OptionsPage} + */ + function EditDictionaryOverlay() { + OptionsPage.call(this, 'editDictionary', + loadTimeData.getString('languageDictionaryOverlayPage'), + 'language-dictionary-overlay-page'); + } + + cr.addSingletonGetter(EditDictionaryOverlay); + + EditDictionaryOverlay.prototype = { + __proto__: OptionsPage.prototype, + + /** + * A list of words in the dictionary. + * @type {DictionaryWordsList} + * @private + */ + wordList_: null, + + /** + * The input field for searching for words in the dictionary. + * @type {HTMLElement} + * @private + */ + searchField_: null, + + /** + * The paragraph of text that indicates that search returned no results. + * @type {HTMLElement} + * @private + */ + noMatchesLabel_: null, + + /** + * Initializes the edit dictionary overlay. + * @override + */ + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + + this.wordList_ = $('language-dictionary-overlay-word-list'); + DictionaryWordsList.decorate(this.wordList_); + this.wordList_.onWordListChanged = function() { + this.onWordListChanged_(); + }.bind(this); + + this.searchField_ = $('language-dictionary-overlay-search-field'); + this.searchField_.onsearch = function(e) { + this.wordList_.search(e.currentTarget.value); + }.bind(this); + this.searchField_.onkeydown = function(e) { + // Don't propagate enter key events. Otherwise the default button will + // activate. + if (e.keyIdentifier == 'Enter') + e.stopPropagation(); + }; + + this.noMatchesLabel_ = getRequiredElement( + 'language-dictionary-overlay-no-matches'); + + $('language-dictionary-overlay-done-button').onclick = function(e) { + OptionsPage.closeOverlay(); + }; + }, + + /** + * Refresh the dictionary words when the page is displayed. + * @override + */ + didShowPage: function() { + chrome.send('refreshDictionaryWords'); + }, + + /** + * Update the view based on the changes in the word list. + * @private + */ + onWordListChanged_: function() { + if (this.searchField_.value.length > 0 && this.wordList_.empty) { + this.noMatchesLabel_.hidden = false; + this.wordList_.classList.add('no-search-matches'); + } else { + this.noMatchesLabel_.hidden = true; + this.wordList_.classList.remove('no-search-matches'); + } + }, + }; + + EditDictionaryOverlay.setWordList = function(entries) { + EditDictionaryOverlay.getInstance().wordList_.setWordList(entries); + }; + + EditDictionaryOverlay.updateWords = function(add_words, remove_words) { + EditDictionaryOverlay.getInstance().wordList_.addWords(add_words); + EditDictionaryOverlay.getInstance().wordList_.removeWords(remove_words); + }; + + EditDictionaryOverlay.getWordListForTesting = function() { + return EditDictionaryOverlay.getInstance().wordList_; + }; + + return { + EditDictionaryOverlay: EditDictionaryOverlay + }; +}); diff --git a/chromium/chrome/browser/resources/options/language_dictionary_overlay_word_list.js b/chromium/chrome/browser/resources/options/language_dictionary_overlay_word_list.js new file mode 100644 index 00000000000..611c56f090f --- /dev/null +++ b/chromium/chrome/browser/resources/options/language_dictionary_overlay_word_list.js @@ -0,0 +1,239 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options.dictionary_words', function() { + /** @const */ var InlineEditableItemList = options.InlineEditableItemList; + /** @const */ var InlineEditableItem = options.InlineEditableItem; + + /** + * Creates a new dictionary word list item. + * @param {string} dictionaryWord The dictionary word. + * @param {function(string)} addDictionaryWordCallback Callback for + * adding a dictionary word. + * @constructor + * @extends {cr.ui.InlineEditableItem} + */ + function DictionaryWordsListItem(dictionaryWord, addDictionaryWordCallback) { + var el = cr.doc.createElement('div'); + el.dictionaryWord_ = dictionaryWord; + el.addDictionaryWordCallback_ = addDictionaryWordCallback; + DictionaryWordsListItem.decorate(el); + return el; + } + + /** + * Decorates an HTML element as a dictionary word list item. + * @param {HTMLElement} el The element to decorate. + */ + DictionaryWordsListItem.decorate = function(el) { + el.__proto__ = DictionaryWordsListItem.prototype; + el.decorate(); + }; + + DictionaryWordsListItem.prototype = { + __proto__: InlineEditableItem.prototype, + + /** @override */ + decorate: function() { + InlineEditableItem.prototype.decorate.call(this); + if (this.dictionaryWord_ == '') + this.isPlaceholder = true; + else + this.editable = false; + + var wordEl = this.createEditableTextCell(this.dictionaryWord_); + wordEl.classList.add('weakrtl'); + wordEl.classList.add('language-dictionary-overlay-word-list-item'); + this.contentElement.appendChild(wordEl); + + var wordField = wordEl.querySelector('input'); + wordField.classList.add('language-dictionary-overlay-word-list-item'); + if (this.isPlaceholder) { + wordField.placeholder = + loadTimeData.getString('languageDictionaryOverlayAddWordLabel'); + } + + this.addEventListener('commitedit', this.onEditCommitted_.bind(this)); + }, + + /** @override */ + get hasBeenEdited() { + return this.querySelector('input').value.length > 0; + }, + + /** + * Adds a word to the dictionary. + * @param {Event} e Edit committed event. + * @private + */ + onEditCommitted_: function(e) { + var input = e.currentTarget.querySelector('input'); + this.addDictionaryWordCallback_(input.value); + input.value = ''; + }, + }; + + /** + * A list of words in the dictionary. + * @extends {cr.ui.InlineEditableItemList} + */ + var DictionaryWordsList = cr.ui.define('list'); + + DictionaryWordsList.prototype = { + __proto__: InlineEditableItemList.prototype, + + /** + * The function to notify that the word list has changed. + * @type {function()} + */ + onWordListChanged: null, + + /** + * The list of all words in the dictionary. Used to generate a filtered word + * list in the |search(searchTerm)| method. + * @type {Array} + * @private + */ + allWordsList_: null, + + /** + * The list of words that the user removed, but |DictionaryWordList| has not + * received a notification of their removal yet. + * @type {Array} + * @private + */ + removedWordsList_: [], + + /** + * Adds a dictionary word. + * @param {string} dictionaryWord The word to add. + * @private + */ + addDictionaryWord_: function(dictionaryWord) { + this.allWordsList_.push(dictionaryWord); + this.dataModel.splice(this.dataModel.length - 1, 0, dictionaryWord); + this.onWordListChanged(); + chrome.send('addDictionaryWord', [dictionaryWord]); + }, + + /** + * Searches the list for the matching words. + * @param {string} searchTerm The search term. + */ + search: function(searchTerm) { + var filteredWordList = this.allWordsList_.filter( + function(element, index, array) { + return element.indexOf(searchTerm) > -1; + }); + filteredWordList.push(''); + this.dataModel = new cr.ui.ArrayDataModel(filteredWordList); + this.onWordListChanged(); + }, + + /** + * Sets the list of dictionary words. + * @param {Array} entries The list of dictionary words. + */ + setWordList: function(entries) { + this.allWordsList_ = entries.slice(0); + // Empty string is a placeholder for entering new words. + entries.push(''); + this.dataModel = new cr.ui.ArrayDataModel(entries); + this.onWordListChanged(); + }, + + /** + * Adds non-duplicate dictionary words. + * @param {Array} entries The list of dictionary words. + */ + addWords: function(entries) { + var toAdd = []; + for (var i = 0; i < entries.length; i++) { + if (this.allWordsList_.indexOf(entries[i]) == -1) { + this.allWordsList_.push(entries[i]); + toAdd.push(entries[i]); + } + } + if (toAdd.length == 0) + return; + for (var i = 0; i < toAdd.length; i++) + this.dataModel.splice(this.dataModel.length - 1, 0, toAdd[i]); + this.onWordListChanged(); + }, + + /** + * Removes dictionary words that are not in |removedWordsList_|. If a word + * is in |removedWordsList_|, then removes the word from there instead. + * @param {Array} entries The list of dictionary words. + */ + removeWords: function(entries) { + var index; + var toRemove = []; + for (var i = 0; i < entries.length; i++) { + index = this.removedWordsList_.indexOf(entries[i]); + if (index > -1) { + this.removedWordsList_.splice(index, 1); + } else { + index = this.allWordsList_.indexOf(entries[i]); + if (index > -1) { + this.allWordsList_.splice(index, 1); + toRemove.push(entries[i]); + } + } + } + if (toRemove.length == 0) + return; + for (var i = 0; i < toRemove.length; i++) { + index = this.dataModel.indexOf(toRemove[i]); + if (index > -1) + this.dataModel.splice(index, 1); + } + this.onWordListChanged(); + }, + + /** + * Returns true if the data model contains no words, otherwise returns + * false. + * @type {boolean} + */ + get empty() { + // A data model without dictionary words contains one placeholder for + // entering new words. + return this.dataModel.length < 2; + }, + + /** @override */ + createItem: function(dictionaryWord) { + return new DictionaryWordsListItem( + dictionaryWord, + this.addDictionaryWord_.bind(this)); + }, + + /** @override */ + deleteItemAtIndex: function(index) { + // The last element in the data model is an undeletable placeholder for + // entering new words. + assert(index > -1 && index < this.dataModel.length - 1); + var item = this.dataModel.item(index); + var allWordsListIndex = this.allWordsList_.indexOf(item); + assert(allWordsListIndex > -1); + this.allWordsList_.splice(allWordsListIndex, 1); + this.dataModel.splice(index, 1); + this.removedWordsList_.push(item); + this.onWordListChanged(); + chrome.send('removeDictionaryWord', [item]); + }, + + /** @override */ + shouldFocusPlaceholder: function() { + return false; + }, + }; + + return { + DictionaryWordsList: DictionaryWordsList + }; + +}); + diff --git a/chromium/chrome/browser/resources/options/language_list.js b/chromium/chrome/browser/resources/options/language_list.js new file mode 100644 index 00000000000..d4ea5bd4f22 --- /dev/null +++ b/chromium/chrome/browser/resources/options/language_list.js @@ -0,0 +1,440 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + /** @const */ var ArrayDataModel = cr.ui.ArrayDataModel; + /** @const */ var DeletableItem = options.DeletableItem; + /** @const */ var DeletableItemList = options.DeletableItemList; + /** @const */ var List = cr.ui.List; + /** @const */ var ListItem = cr.ui.ListItem; + /** @const */ var ListSingleSelectionModel = cr.ui.ListSingleSelectionModel; + + /** + * Creates a new Language list item. + * @param {Object} languageInfo The information of the language. + * @constructor + * @extends {DeletableItem.ListItem} + */ + function LanguageListItem(languageInfo) { + var el = cr.doc.createElement('li'); + el.__proto__ = LanguageListItem.prototype; + el.language_ = languageInfo; + el.decorate(); + return el; + }; + + LanguageListItem.prototype = { + __proto__: DeletableItem.prototype, + + /** + * The language code of this language. + * @type {string} + * @private + */ + languageCode_: null, + + /** @override */ + decorate: function() { + DeletableItem.prototype.decorate.call(this); + + var languageCode = this.language_.code; + var languageOptions = options.LanguageOptions.getInstance(); + this.deletable = languageOptions.languageIsDeletable(languageCode); + this.languageCode = languageCode; + this.languageName = cr.doc.createElement('div'); + this.languageName.className = 'language-name'; + this.languageName.dir = this.language_.textDirection; + this.languageName.textContent = this.language_.displayName; + this.contentElement.appendChild(this.languageName); + this.title = this.language_.nativeDisplayName; + this.draggable = true; + }, + }; + + /** + * Creates a new language list. + * @param {Object=} opt_propertyBag Optional properties. + * @constructor + * @extends {cr.ui.List} + */ + var LanguageList = cr.ui.define('list'); + + /** + * Gets information of a language from the given language code. + * @param {string} languageCode Language code (ex. "fr"). + */ + LanguageList.getLanguageInfoFromLanguageCode = function(languageCode) { + // Build the language code to language info dictionary at first time. + if (!this.languageCodeToLanguageInfo_) { + this.languageCodeToLanguageInfo_ = {}; + var languageList = loadTimeData.getValue('languageList'); + for (var i = 0; i < languageList.length; i++) { + var languageInfo = languageList[i]; + this.languageCodeToLanguageInfo_[languageInfo.code] = languageInfo; + } + } + + return this.languageCodeToLanguageInfo_[languageCode]; + } + + /** + * Returns true if the given language code is valid. + * @param {string} languageCode Language code (ex. "fr"). + */ + LanguageList.isValidLanguageCode = function(languageCode) { + // Having the display name for the language code means that the + // language code is valid. + if (LanguageList.getLanguageInfoFromLanguageCode(languageCode)) { + return true; + } + return false; + } + + LanguageList.prototype = { + __proto__: DeletableItemList.prototype, + + // The list item being dragged. + draggedItem: null, + // The drop position information: "below" or "above". + dropPos: null, + // The preference is a CSV string that describes preferred languages + // in Chrome OS. The language list is used for showing the language + // list in "Language and Input" options page. + preferredLanguagesPref: 'settings.language.preferred_languages', + // The preference is a CSV string that describes accept languages used + // for content negotiation. To be more precise, the list will be used + // in "Accept-Language" header in HTTP requests. + acceptLanguagesPref: 'intl.accept_languages', + + /** @override */ + decorate: function() { + DeletableItemList.prototype.decorate.call(this); + this.selectionModel = new ListSingleSelectionModel; + + // HACK(arv): http://crbug.com/40902 + window.addEventListener('resize', this.redraw.bind(this)); + + // Listen to pref change. + if (cr.isChromeOS) { + Preferences.getInstance().addEventListener(this.preferredLanguagesPref, + this.handlePreferredLanguagesPrefChange_.bind(this)); + } else { + Preferences.getInstance().addEventListener(this.acceptLanguagesPref, + this.handleAcceptLanguagesPrefChange_.bind(this)); + } + + // Listen to drag and drop events. + this.addEventListener('dragstart', this.handleDragStart_.bind(this)); + this.addEventListener('dragenter', this.handleDragEnter_.bind(this)); + this.addEventListener('dragover', this.handleDragOver_.bind(this)); + this.addEventListener('drop', this.handleDrop_.bind(this)); + this.addEventListener('dragleave', this.handleDragLeave_.bind(this)); + }, + + createItem: function(languageCode) { + languageInfo = LanguageList.getLanguageInfoFromLanguageCode(languageCode); + return new LanguageListItem(languageInfo); + }, + + /* + * For each item, determines whether it's deletable. + */ + updateDeletable: function() { + var items = this.items; + for (var i = 0; i < items.length; ++i) { + var item = items[i]; + var languageCode = item.languageCode; + var languageOptions = options.LanguageOptions.getInstance(); + item.deletable = languageOptions.languageIsDeletable(languageCode); + } + }, + + /* + * Adds a language to the language list. + * @param {string} languageCode language code (ex. "fr"). + */ + addLanguage: function(languageCode) { + // It shouldn't happen but ignore the language code if it's + // null/undefined, or already present. + if (!languageCode || this.dataModel.indexOf(languageCode) >= 0) { + return; + } + this.dataModel.push(languageCode); + // Select the last item, which is the language added. + this.selectionModel.selectedIndex = this.dataModel.length - 1; + + this.savePreference_(); + }, + + /* + * Gets the language codes of the currently listed languages. + */ + getLanguageCodes: function() { + return this.dataModel.slice(); + }, + + /* + * Clears the selection + */ + clearSelection: function() { + this.selectionModel.unselectAll(); + }, + + /* + * Gets the language code of the selected language. + */ + getSelectedLanguageCode: function() { + return this.selectedItem; + }, + + /* + * Selects the language by the given language code. + * @returns {boolean} True if the operation is successful. + */ + selectLanguageByCode: function(languageCode) { + var index = this.dataModel.indexOf(languageCode); + if (index >= 0) { + this.selectionModel.selectedIndex = index; + return true; + } + return false; + }, + + /** @override */ + deleteItemAtIndex: function(index) { + if (index >= 0) { + this.dataModel.splice(index, 1); + // Once the selected item is removed, there will be no selected item. + // Select the item pointed by the lead index. + index = this.selectionModel.leadIndex; + this.savePreference_(); + } + return index; + }, + + /* + * Computes the target item of drop event. + * @param {Event} e The drop or dragover event. + * @private + */ + getTargetFromDropEvent_: function(e) { + var target = e.target; + // e.target may be an inner element of the list item + while (target != null && !(target instanceof ListItem)) { + target = target.parentNode; + } + return target; + }, + + /* + * Handles the dragstart event. + * @param {Event} e The dragstart event. + * @private + */ + handleDragStart_: function(e) { + var target = e.target; + // ListItem should be the only draggable element type in the page, + // but just in case. + if (target instanceof ListItem) { + this.draggedItem = target; + e.dataTransfer.effectAllowed = 'move'; + // We need to put some kind of data in the drag or it will be + // ignored. Use the display name in case the user drags to a text + // field or the desktop. + e.dataTransfer.setData('text/plain', target.title); + } + }, + + /* + * Handles the dragenter event. + * @param {Event} e The dragenter event. + * @private + */ + handleDragEnter_: function(e) { + e.preventDefault(); + }, + + /* + * Handles the dragover event. + * @param {Event} e The dragover event. + * @private + */ + handleDragOver_: function(e) { + var dropTarget = this.getTargetFromDropEvent_(e); + // Determines whether the drop target is to accept the drop. + // The drop is only successful on another ListItem. + if (!(dropTarget instanceof ListItem) || + dropTarget == this.draggedItem) { + this.hideDropMarker_(); + return; + } + // Compute the drop postion. Should we move the dragged item to + // below or above the drop target? + var rect = dropTarget.getBoundingClientRect(); + var dy = e.clientY - rect.top; + var yRatio = dy / rect.height; + var dropPos = yRatio <= .5 ? 'above' : 'below'; + this.dropPos = dropPos; + this.showDropMarker_(dropTarget, dropPos); + e.preventDefault(); + }, + + /* + * Handles the drop event. + * @param {Event} e The drop event. + * @private + */ + handleDrop_: function(e) { + var dropTarget = this.getTargetFromDropEvent_(e); + this.hideDropMarker_(); + + // Delete the language from the original position. + var languageCode = this.draggedItem.languageCode; + var originalIndex = this.dataModel.indexOf(languageCode); + this.dataModel.splice(originalIndex, 1); + // Insert the language to the new position. + var newIndex = this.dataModel.indexOf(dropTarget.languageCode); + if (this.dropPos == 'below') + newIndex += 1; + this.dataModel.splice(newIndex, 0, languageCode); + // The cursor should move to the moved item. + this.selectionModel.selectedIndex = newIndex; + // Save the preference. + this.savePreference_(); + }, + + /* + * Handles the dragleave event. + * @param {Event} e The dragleave event + * @private + */ + handleDragLeave_: function(e) { + this.hideDropMarker_(); + }, + + /* + * Shows and positions the marker to indicate the drop target. + * @param {HTMLElement} target The current target list item of drop + * @param {string} pos 'below' or 'above' + * @private + */ + showDropMarker_: function(target, pos) { + window.clearTimeout(this.hideDropMarkerTimer_); + var marker = $('language-options-list-dropmarker'); + var rect = target.getBoundingClientRect(); + var markerHeight = 8; + if (pos == 'above') { + marker.style.top = (rect.top - markerHeight / 2) + 'px'; + } else { + marker.style.top = (rect.bottom - markerHeight / 2) + 'px'; + } + marker.style.width = rect.width + 'px'; + marker.style.left = rect.left + 'px'; + marker.style.display = 'block'; + }, + + /* + * Hides the drop marker. + * @private + */ + hideDropMarker_: function() { + // Hide the marker in a timeout to reduce flickering as we move between + // valid drop targets. + window.clearTimeout(this.hideDropMarkerTimer_); + this.hideDropMarkerTimer_ = window.setTimeout(function() { + $('language-options-list-dropmarker').style.display = ''; + }, 100); + }, + + /** + * Handles preferred languages pref change. + * @param {Event} e The change event object. + * @private + */ + handlePreferredLanguagesPrefChange_: function(e) { + var languageCodesInCsv = e.value.value; + var languageCodes = languageCodesInCsv.split(','); + + // Add the UI language to the initial list of languages. This is to avoid + // a bug where the UI language would be removed from the preferred + // language list by sync on first login. + // See: crosbug.com/14283 + languageCodes.push(navigator.language); + languageCodes = this.filterBadLanguageCodes_(languageCodes); + this.load_(languageCodes); + }, + + /** + * Handles accept languages pref change. + * @param {Event} e The change event object. + * @private + */ + handleAcceptLanguagesPrefChange_: function(e) { + var languageCodesInCsv = e.value.value; + var languageCodes = this.filterBadLanguageCodes_( + languageCodesInCsv.split(',')); + this.load_(languageCodes); + }, + + /** + * Loads given language list. + * @param {Array} languageCodes List of language codes. + * @private + */ + load_: function(languageCodes) { + // Preserve the original selected index. See comments below. + var originalSelectedIndex = (this.selectionModel ? + this.selectionModel.selectedIndex : -1); + this.dataModel = new ArrayDataModel(languageCodes); + if (originalSelectedIndex >= 0 && + originalSelectedIndex < this.dataModel.length) { + // Restore the original selected index if the selected index is + // valid after the data model is loaded. This is neeeded to keep + // the selected language after the languge is added or removed. + this.selectionModel.selectedIndex = originalSelectedIndex; + // The lead index should be updated too. + this.selectionModel.leadIndex = originalSelectedIndex; + } else if (this.dataModel.length > 0) { + // Otherwise, select the first item if it's not empty. + // Note that ListSingleSelectionModel won't select an item + // automatically, hence we manually select the first item here. + this.selectionModel.selectedIndex = 0; + } + }, + + /** + * Saves the preference. + */ + savePreference_: function() { + chrome.send('updateLanguageList', [this.dataModel.slice()]); + cr.dispatchSimpleEvent(this, 'save'); + }, + + /** + * Filters bad language codes in case bad language codes are + * stored in the preference. Removes duplicates as well. + * @param {Array} languageCodes List of language codes. + * @private + */ + filterBadLanguageCodes_: function(languageCodes) { + var filteredLanguageCodes = []; + var seen = {}; + for (var i = 0; i < languageCodes.length; i++) { + // Check if the the language code is valid, and not + // duplicate. Otherwise, skip it. + if (LanguageList.isValidLanguageCode(languageCodes[i]) && + !(languageCodes[i] in seen)) { + filteredLanguageCodes.push(languageCodes[i]); + seen[languageCodes[i]] = true; + } + } + return filteredLanguageCodes; + }, + }; + + return { + LanguageList: LanguageList, + LanguageListItem: LanguageListItem + }; +}); diff --git a/chromium/chrome/browser/resources/options/language_options.css b/chromium/chrome/browser/resources/options/language_options.css new file mode 100644 index 00000000000..4e86bbbe02b --- /dev/null +++ b/chromium/chrome/browser/resources/options/language_options.css @@ -0,0 +1,191 @@ +/* Copyright (c) 2012 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ + +.language-options { + display: -webkit-box; + margin: 10px 0; +} + +.language-options-lower-left button, +#language-options-details button { + min-width: 70px; +} + +.language-options h3 { + -webkit-margin-start: 12px; + font-size: 100%; + font-weight: bold; + margin-bottom: 12px; + margin-top: 12px; +} + +.language-options-contents { + min-height: 28px; + padding: 0 12px 4px; +} + +.language-options-contents > span:not(.input-method-label) { + display: inline-block; + margin: 1px; + padding: 0.42em 10px; +} + +.language-options-header, +.language-options-footer { + line-height: 1.2em; + margin: 10px 0; +} + +#language-options-languages, +#language-options-details { + border: 1px solid #ccc; + height: 400px; + padding: 0; + vertical-align: top; +} + +#language-options-languages { + -webkit-box-orient: vertical; + background-color: rgb(235, 239, 249); + display: -webkit-box; + width: 300px; +} + +.language-options-lower-left { + -webkit-box-flex: 0; + -webkit-padding-start: 12px; + padding-bottom: 10px; +} + +#language-options-details { + /* To share the center line with the left pane. */ + -webkit-margin-start: -1px; + width: 360px; +} + +#language-options-details h3:not(:first-of-type) { + margin-top: 24px; +} + +.language-options-notification { + background-color: rgb(255, 247, 193); + margin: 0 0 4px; + padding: 8px 30px 8px 12px; +} + +.language-options-notification > div { + margin-bottom: 4px; +} + +#language-options-input-method-list button { + -webkit-margin-start: 20px; + display: block; + /* Same margin as .settings-row. */ + margin-bottom: 0.65em; + margin-top: 0.65em; +} + +#language-options-list { + -webkit-box-flex: 1; + outline: none; + padding: 0; + width: 100%; +} + +#language-options-list .language-name { + -webkit-box-flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +#language-options-list li { + -webkit-padding-start: 12px; + padding-bottom: 2px; + padding-top: 2px; +} + +#language-options-list-dropmarker { + background-clip: padding-box; + background-color: hsl(214, 91%, 65%); + border: 3px solid hsl(214, 91%, 65%); + border-bottom-color: transparent; + border-radius: 0; + border-top-color: transparent; + box-sizing: border-box; + display: none; + height: 8px; + overflow: hidden; + pointer-events: none; + position: fixed; + z-index: 10; +} + +/* TODO(kochi): This is temporary copy from new_tab.css */ +/* Notification */ + +#notification { + -webkit-transition: opacity 150ms; + background-color: hsl(52, 100%, 80%); + border: 1px solid rgb(211, 211, 211); + border-radius: 6px; + color: black; + display: table; + font-weight: bold; + /* Set the height and margin so that the element does not use any vertical + space. */ + height: 16px; + margin: -44px auto 12px auto; + opacity: 0; + padding: 7px 15px; + pointer-events: none; + position: relative; + white-space: nowrap; + z-index: 1; +} + +#notification > * { + display: table-cell; + max-width: 500px; + overflow: hidden; + text-overflow: ellipsis; +} + +#notification.show { + -webkit-transition: opacity 1s; + opacity: 1; + pointer-events: all; +} + +#notification .link { + -webkit-appearance: none; + -webkit-padding-start: 20px; + background: none; + border: 0; + color: rgba(0, 102, 204, 0.3); + cursor: pointer; + text-decoration: underline; +} + +#notification .link-color { + color: rgb(0, 102, 204); +} + +#chewing-max-chi-symbol-len { + height: 30%; + width: 100px; +} + +#add-language-overlay-page .content-area { + padding-bottom: 10px; +} + +/* TODO(hshi): Remove this temporary hack once the bug is fixed in Chrome. */ +#add-language-overlay-language-list { + width: -webkit-calc(100% - 4px); +} + +.standalone-link-button { + padding: 0; +} diff --git a/chromium/chrome/browser/resources/options/language_options.html b/chromium/chrome/browser/resources/options/language_options.html new file mode 100644 index 00000000000..e2d420cb61a --- /dev/null +++ b/chromium/chrome/browser/resources/options/language_options.html @@ -0,0 +1,136 @@ +<div id="languagePage" class="page" hidden> + <div class="close-button"></div> + <h1 i18n-content="languagePage"></h1> + <div class="content-area"> + <div id="notification"> + <span> </span> + <span class="link"><span class="link-color"></span></span> + </div> + <div class="language-options-header"> + <div i18n-content="addLanguageInstructions"></div> +<if expr="pp_ifdef('chromeos')"> + <div i18n-content="inputMethodInstructions"></div> +</if> + </div> + <div class="language-options"> + <div id="language-options-languages"> + <h3 i18n-content="languages"></h3> + <list id="language-options-list"></list> + <div class="language-options-lower-left"> + <button id="language-options-add-button" + i18n-content="addButton"> + </button> + </div> + <div id="language-options-list-dropmarker"></div> + </div> + <div id="language-options-details"> + <h3 id="language-options-language-name"></h3> +<if expr="os == 'win32' or pp_ifdef('chromeos')"> + <div class="language-options-contents"> + <button id="language-options-ui-language-button" + i18n-content="displayInThisLanguage"> + </button> + <span id="language-options-ui-language-message" hidden></span> + </div> +</if> +<!-- Chrome uses the native OS spellchecker in OS X, so don't display the + dictionary pane. --> +<if expr="not is_macosx"> + <div id="language-options-spellcheck" class="language-options-contents"> + <button id="language-options-spell-check-language-button" + i18n-content="useThisForSpellChecking"> + </button> + <span id="language-options-spell-check-language-message" hidden> + </span> + <span id="language-options-dictionary-downloading-message" + i18n-content="downloadingDictionary" hidden> + </span> + </div> + <div id="language-options-dictionary-download-failed-message" + class="language-options-notification" hidden> + <div i18n-content="downloadFailed"></div> + <div id="language-options-dictionary-download-fail-help-message" + i18n-content="downloadFailHelp" hidden> + </div> + <button id="dictionary-download-retry-button" + i18n-content="retryButton"> + </button> + </div> + <div id="language-options-ui-notification-bar" + class="language-options-notification" hidden> + <div i18n-content="restartRequired"></div> +<if expr="pp_ifdef('chromeos')"> + <button id="language-options-ui-restart-button" + i18n-content="restartButton"> + </button> +</if> + </div> +</if> + <div id="language-options-offer-to-translate" + class="language-options-contents" hidden> + <div class="checkbox"> + <label> + <input type="checkbox" id="offer-to-translate-in-this-language"> + <span class="offer-to-translate-label" + i18n-content="offerToTranslateInThisLanguage"></span> + </label> + </div> + <span id="cannot-translate-in-this-language" + i18n-content="cannotTranslateInThisLanguage" hidden></span> + </div> +<if expr="pp_ifdef('chromeos')"> + <h3 i18n-content="inputMethod"></h3> + <div id="language-options-input-method-template" class="input-method" + hidden> + <div class="checkbox"> + <label> + <input type="checkbox"> + <span class="input-method-label"></span> + </label> + </div> + </div> + <div id="language-options-input-method-list" + class="language-options-contents"> + <span id="language-options-input-method-none" + i18n-content="noInputMethods" hidden> + </span> + </div> +</if> + </div> + </div> + <div class="language-options-footer"> +<if expr="pp_ifdef('chromeos')"> + <div i18n-content="switchInputMethodsHint"></div> + <div i18n-content="selectPreviousInputMethodHint"></div> + <button id="edit-dictionary-button" + class="link-button standalone-link-button" + i18n-content="languageDictionaryOverlayTitle"></button> +</if> +<if expr="not pp_ifdef('chromeos') and not is_macosx"> + <div id="spell-check-option" class="checkbox"> + <label> + <input id="enable-spell-check" pref="browser.enable_spellchecking" + metric="Options_SpellCheck" type="checkbox"> + <span i18n-content="enableSpellCheck"></span> + </label> + <button id="edit-dictionary-button" class="link-button" + i18n-content="languageDictionaryOverlayTitle" hidden></button> + </div> + <div id="auto-spell-correction-option" class="checkbox" hidden> + <label> + <input id="enable-auto-spell-correction" + pref="browser.enable_autospellcorrect" + metric="Options_AutoSpellCorrection" type="checkbox"> + <span i18n-content="enableAutoSpellCorrection"></span> + </label> + </div> +</if> + </div> + </div> + <div class="action-area"> + <div class="button-strip"> + <button id="language-confirm" class="default-button" i18n-content="done"> + </button> + </div> + </div> +</div> diff --git a/chromium/chrome/browser/resources/options/language_options.js b/chromium/chrome/browser/resources/options/language_options.js new file mode 100644 index 00000000000..3ebf7d2665b --- /dev/null +++ b/chromium/chrome/browser/resources/options/language_options.js @@ -0,0 +1,1304 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// TODO(kochi): Generalize the notification as a component and put it +// in js/cr/ui/notification.js . + +cr.define('options', function() { + /** @const */ var OptionsPage = options.OptionsPage; + /** @const */ var LanguageList = options.LanguageList; + + /** + * Spell check dictionary download status. + * @type {Enum} + */ + /** @const*/ var DOWNLOAD_STATUS = { + IN_PROGRESS: 1, + FAILED: 2 + }; + + /** + * The preference is a boolean that enables/disables spell checking. + * @type {string} + * @const + */ + var ENABLE_SPELL_CHECK_PREF = 'browser.enable_spellchecking'; + + /** + * The preference is a CSV string that describes preload engines + * (i.e. active input methods). + * @type {string} + * @const + */ + var PRELOAD_ENGINES_PREF = 'settings.language.preload_engines'; + + /** + * The preference that lists the extension IMEs that are enabled in the + * language menu. + * @type {string} + * @const + */ + var ENABLED_EXTENSION_IME_PREF = 'settings.language.enabled_extension_imes'; + + /** + * The preference that lists the languages which are not translated. + * @type {string} + * @const + */ + var TRANSLATE_BLOCKED_LANGUAGES_PREF = 'translate_blocked_languages'; + + /** + * The preference key that is a string that describes the spell check + * dictionary language, like "en-US". + * @type {string} + * @const + */ + var SPELL_CHECK_DICTIONARY_PREF = 'spellcheck.dictionary'; + + /** + * The preference that indicates if the Translate feature is enabled. + * @type {string} + * @const + */ + var ENABLE_TRANSLATE = 'translate.enabled'; + + ///////////////////////////////////////////////////////////////////////////// + // LanguageOptions class: + + /** + * Encapsulated handling of ChromeOS language options page. + * @constructor + */ + function LanguageOptions(model) { + OptionsPage.call(this, 'languages', + loadTimeData.getString('languagePageTabTitle'), + 'languagePage'); + } + + cr.addSingletonGetter(LanguageOptions); + + // Inherit LanguageOptions from OptionsPage. + LanguageOptions.prototype = { + __proto__: OptionsPage.prototype, + + /* For recording the prospective language (the next locale after relaunch). + * @type {?string} + * @private + */ + prospectiveUiLanguageCode_: null, + + /* + * Map from language code to spell check dictionary download status for that + * language. + * @type {Array} + * @private + */ + spellcheckDictionaryDownloadStatus_: [], + + /** + * Number of times a spell check dictionary download failed. + * @type {int} + * @private + */ + spellcheckDictionaryDownloadFailures_: 0, + + /** + * The list of preload engines, like ['mozc', 'pinyin']. + * @type {Array} + * @private + */ + preloadEngines_: [], + + /** + * The list of extension IMEs that are enabled out of the language menu. + * @type {Array} + * @private + */ + enabledExtensionImes_: [], + + /** + * The list of the languages which is not translated. + * @type {Array} + * @private + */ + translateBlockedLanguages_: [], + + /** + * The list of the languages supported by Translate server + * @type {Array} + * @private + */ + translateSupportedLanguages_: [], + + /** + * The preference is a string that describes the spell check dictionary + * language, like "en-US". + * @type {string} + * @private + */ + spellCheckDictionary_: '', + + /** + * The map of language code to input method IDs, like: + * {'ja': ['mozc', 'mozc-jp'], 'zh-CN': ['pinyin'], ...} + * @type {Object} + * @private + */ + languageCodeToInputMethodIdsMap_: {}, + + /** + * The value that indicates if Translate feature is enabled or not. + * @type {boolean} + * @private + */ + enableTranslate_: false, + + /** + * Initializes LanguageOptions page. + * Calls base class implementation to start preference initialization. + */ + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + + var languageOptionsList = $('language-options-list'); + LanguageList.decorate(languageOptionsList); + + languageOptionsList.addEventListener('change', + this.handleLanguageOptionsListChange_.bind(this)); + languageOptionsList.addEventListener('save', + this.handleLanguageOptionsListSave_.bind(this)); + + this.prospectiveUiLanguageCode_ = + loadTimeData.getString('prospectiveUiLanguageCode'); + this.addEventListener('visibleChange', + this.handleVisibleChange_.bind(this)); + + if (cr.isChromeOS) { + this.initializeInputMethodList_(); + this.initializeLanguageCodeToInputMethodIdsMap_(); + } + + var checkbox = $('offer-to-translate-in-this-language'); + checkbox.addEventListener('click', + this.handleOfferToTranslateCheckboxClick_.bind(this)); + + Preferences.getInstance().addEventListener( + TRANSLATE_BLOCKED_LANGUAGES_PREF, + this.handleTranslateBlockedLanguagesPrefChange_.bind(this)); + Preferences.getInstance().addEventListener(SPELL_CHECK_DICTIONARY_PREF, + this.handleSpellCheckDictionaryPrefChange_.bind(this)); + Preferences.getInstance().addEventListener(ENABLE_TRANSLATE, + this.handleEnableTranslatePrefChange_.bind(this)); + this.translateSupportedLanguages_ = + loadTimeData.getValue('translateSupportedLanguages'); + + // Set up add button. + $('language-options-add-button').onclick = function(e) { + // Add the language without showing the overlay if it's specified in + // the URL hash (ex. lang_add=ja). Used for automated testing. + var match = document.location.hash.match(/\blang_add=([\w-]+)/); + if (match) { + var addLanguageCode = match[1]; + $('language-options-list').addLanguage(addLanguageCode); + this.addBlockedLanguage_(addLanguageCode); + } else { + OptionsPage.navigateToPage('addLanguage'); + } + }.bind(this); + + if (!cr.isMac) { + // Set up the button for editing custom spelling dictionary. + $('edit-dictionary-button').onclick = function(e) { + OptionsPage.navigateToPage('editDictionary'); + }; + $('dictionary-download-retry-button').onclick = function(e) { + chrome.send('retryDictionaryDownload'); + }; + } + + // Listen to add language dialog ok button. + $('add-language-overlay-ok-button').addEventListener( + 'click', this.handleAddLanguageOkButtonClick_.bind(this)); + + if (!cr.isChromeOS) { + // Show experimental features if enabled. + if (loadTimeData.getBoolean('enableSpellingAutoCorrect')) + $('auto-spell-correction-option').hidden = false; + + // Handle spell check enable/disable. + if (!cr.isMac) { + Preferences.getInstance().addEventListener( + ENABLE_SPELL_CHECK_PREF, + this.updateEnableSpellCheck_.bind(this)); + } + } + + // Handle clicks on "Use this language for spell checking" button. + if (!cr.isMac) { + var spellCheckLanguageButton = getRequiredElement( + 'language-options-spell-check-language-button'); + spellCheckLanguageButton.addEventListener( + 'click', + this.handleSpellCheckLanguageButtonClick_.bind(this)); + } + + if (cr.isChromeOS) { + $('language-options-ui-restart-button').onclick = function() { + chrome.send('uiLanguageRestart'); + }; + } + + $('language-confirm').onclick = + OptionsPage.closeOverlay.bind(OptionsPage); + }, + + /** + * Initializes the input method list. + */ + initializeInputMethodList_: function() { + var inputMethodList = $('language-options-input-method-list'); + var inputMethodPrototype = $('language-options-input-method-template'); + + // Add all input methods, but make all of them invisible here. We'll + // change the visibility in handleLanguageOptionsListChange_() based + // on the selected language. Note that we only have less than 100 + // input methods, so creating DOM nodes at once here should be ok. + this.appendInputMethodElement_(loadTimeData.getValue('inputMethodList')); + this.appendInputMethodElement_(loadTimeData.getValue('extensionImeList')); + this.appendComponentExtensionIme_( + loadTimeData.getValue('componentExtensionImeList')); + + // Listen to pref change once the input method list is initialized. + Preferences.getInstance().addEventListener( + PRELOAD_ENGINES_PREF, + this.handlePreloadEnginesPrefChange_.bind(this)); + Preferences.getInstance().addEventListener( + ENABLED_EXTENSION_IME_PREF, + this.handleEnabledExtensionsPrefChange_.bind(this)); + }, + + /** + * Appends input method lists based on component extension ime list. + * @param {!Array} componentExtensionImeList A list of input method + * descriptors. + * @private + */ + appendComponentExtensionIme_: function(componentExtensionImeList) { + this.appendInputMethodElement_(componentExtensionImeList); + + for (var i = 0; i < componentExtensionImeList.length; i++) { + var inputMethod = componentExtensionImeList[i]; + for (var languageCode in inputMethod.languageCodeSet) { + if (languageCode in this.languageCodeToInputMethodIdsMap_) { + this.languageCodeToInputMethodIdsMap_[languageCode].push( + inputMethod.id); + } else { + this.languageCodeToInputMethodIdsMap_[languageCode] = + [inputMethod.id]; + } + } + } + }, + + /** + * Appends input methods into input method list. + * @param {!Array} inputMethods A list of input method descriptors. + * @private + */ + appendInputMethodElement_: function(inputMethods) { + var inputMethodList = $('language-options-input-method-list'); + var inputMethodTemplate = $('language-options-input-method-template'); + + for (var i = 0; i < inputMethods.length; i++) { + var inputMethod = inputMethods[i]; + var element = inputMethodTemplate.cloneNode(true); + element.id = ''; + element.languageCodeSet = inputMethod.languageCodeSet; + + var input = element.querySelector('input'); + input.inputMethodId = inputMethod.id; + var span = element.querySelector('span'); + span.textContent = inputMethod.displayName; + + if (inputMethod.optionsPage) { + var button = document.createElement('button'); + button.textContent = loadTimeData.getString('configure'); + button.inputMethodId = inputMethod.id; + button.onclick = function(inputMethodId, e) { + chrome.send('inputMethodOptionsOpen', [inputMethodId]); + }.bind(this, inputMethod.id); + element.appendChild(button); + } + + // Listen to user clicks. + input.addEventListener('click', + this.handleCheckboxClick_.bind(this)); + inputMethodList.appendChild(element); + } + }, + + /** + * Adds a language to the preference 'translate_blocked_languages'. If + * |langCode| is already added, nothing happens. |langCode| is converted + * to a Translate language synonym before added. + * @param {string} langCode A language code like 'en' + * @private + */ + addBlockedLanguage_: function(langCode) { + langCode = this.convertLangCodeForTranslation_(langCode); + if (this.translateBlockedLanguages_.indexOf(langCode) == -1) { + this.translateBlockedLanguages_.push(langCode); + Preferences.setListPref(TRANSLATE_BLOCKED_LANGUAGES_PREF, + this.translateBlockedLanguages_, true); + } + }, + + /** + * Removes a language from the preference 'translate_blocked_languages'. + * If |langCode| doesn't exist in the preference, nothing happens. + * |langCode| is converted to a Translate language synonym before removed. + * @param {string} langCode A language code like 'en' + * @private + */ + removeBlockedLanguage_: function(langCode) { + langCode = this.convertLangCodeForTranslation_(langCode); + if (this.translateBlockedLanguages_.indexOf(langCode) != -1) { + this.translateBlockedLanguages_ = + this.translateBlockedLanguages_.filter( + function(langCodeNotTranslated) { + return langCodeNotTranslated != langCode; + }); + Preferences.setListPref(TRANSLATE_BLOCKED_LANGUAGES_PREF, + this.translateBlockedLanguages_, true); + } + }, + + /** + * Handles OptionsPage's visible property change event. + * @param {Event} e Property change event. + * @private + */ + handleVisibleChange_: function(e) { + if (this.visible) { + $('language-options-list').redraw(); + chrome.send('languageOptionsOpen'); + } + }, + + /** + * Handles languageOptionsList's change event. + * @param {Event} e Change event. + * @private + */ + handleLanguageOptionsListChange_: function(e) { + var languageOptionsList = $('language-options-list'); + var languageCode = languageOptionsList.getSelectedLanguageCode(); + + // If there's no selection, just return. + if (!languageCode) + return; + + // Select the language if it's specified in the URL hash (ex. lang=ja). + // Used for automated testing. + var match = document.location.hash.match(/\blang=([\w-]+)/); + if (match) { + var specifiedLanguageCode = match[1]; + if (languageOptionsList.selectLanguageByCode(specifiedLanguageCode)) { + languageCode = specifiedLanguageCode; + } + } + + this.updateOfferToTranslateCheckbox_(languageCode); + + if (cr.isWindows || cr.isChromeOS) + this.updateUiLanguageButton_(languageCode); + + this.updateSelectedLanguageName_(languageCode); + + if (!cr.isMac) + this.updateSpellCheckLanguageButton_(languageCode); + + if (cr.isChromeOS) + this.updateInputMethodList_(languageCode); + + this.updateLanguageListInAddLanguageOverlay_(); + }, + + /** + * Happens when a user changes back to the language they're currently using. + */ + currentLocaleWasReselected: function() { + this.updateUiLanguageButton_( + loadTimeData.getString('currentUiLanguageCode')); + }, + + /** + * Handles languageOptionsList's save event. + * @param {Event} e Save event. + * @private + */ + handleLanguageOptionsListSave_: function(e) { + if (cr.isChromeOS) { + // Sort the preload engines per the saved languages before save. + this.preloadEngines_ = this.sortPreloadEngines_(this.preloadEngines_); + this.savePreloadEnginesPref_(); + } + }, + + /** + * Sorts preloadEngines_ by languageOptionsList's order. + * @param {Array} preloadEngines List of preload engines. + * @return {Array} Returns sorted preloadEngines. + * @private + */ + sortPreloadEngines_: function(preloadEngines) { + // For instance, suppose we have two languages and associated input + // methods: + // + // - Korean: hangul + // - Chinese: pinyin + // + // The preloadEngines preference should look like "hangul,pinyin". + // If the user reverse the order, the preference should be reorderd + // to "pinyin,hangul". + var languageOptionsList = $('language-options-list'); + var languageCodes = languageOptionsList.getLanguageCodes(); + + // Convert the list into a dictonary for simpler lookup. + var preloadEngineSet = {}; + for (var i = 0; i < preloadEngines.length; i++) { + preloadEngineSet[preloadEngines[i]] = true; + } + + // Create the new preload engine list per the language codes. + var newPreloadEngines = []; + for (var i = 0; i < languageCodes.length; i++) { + var languageCode = languageCodes[i]; + var inputMethodIds = this.languageCodeToInputMethodIdsMap_[ + languageCode]; + if (!inputMethodIds) + continue; + + // Check if we have active input methods associated with the language. + for (var j = 0; j < inputMethodIds.length; j++) { + var inputMethodId = inputMethodIds[j]; + if (inputMethodId in preloadEngineSet) { + // If we have, add it to the new engine list. + newPreloadEngines.push(inputMethodId); + // And delete it from the set. This is necessary as one input + // method can be associated with more than one language thus + // we should avoid having duplicates in the new list. + delete preloadEngineSet[inputMethodId]; + } + } + } + + return newPreloadEngines; + }, + + /** + * Initializes the map of language code to input method IDs. + * @private + */ + initializeLanguageCodeToInputMethodIdsMap_: function() { + var inputMethodList = loadTimeData.getValue('inputMethodList'); + for (var i = 0; i < inputMethodList.length; i++) { + var inputMethod = inputMethodList[i]; + for (var languageCode in inputMethod.languageCodeSet) { + if (languageCode in this.languageCodeToInputMethodIdsMap_) { + this.languageCodeToInputMethodIdsMap_[languageCode].push( + inputMethod.id); + } else { + this.languageCodeToInputMethodIdsMap_[languageCode] = + [inputMethod.id]; + } + } + } + }, + + /** + * Updates the currently selected language name. + * @param {string} languageCode Language code (ex. "fr"). + * @private + */ + updateSelectedLanguageName_: function(languageCode) { + var languageInfo = LanguageList.getLanguageInfoFromLanguageCode( + languageCode); + var languageDisplayName = languageInfo.displayName; + var languageNativeDisplayName = languageInfo.nativeDisplayName; + var textDirection = languageInfo.textDirection; + + // If the native name is different, add it. + if (languageDisplayName != languageNativeDisplayName) { + languageDisplayName += ' - ' + languageNativeDisplayName; + } + + // Update the currently selected language name. + var languageName = $('language-options-language-name'); + languageName.textContent = languageDisplayName; + languageName.dir = textDirection; + }, + + /** + * Updates the UI language button. + * @param {string} languageCode Language code (ex. "fr"). + * @private + */ + updateUiLanguageButton_: function(languageCode) { + var uiLanguageButton = $('language-options-ui-language-button'); + var uiLanguageMessage = $('language-options-ui-language-message'); + var uiLanguageNotification = $('language-options-ui-notification-bar'); + + // Remove the event listener and add it back if useful. + uiLanguageButton.onclick = null; + + // Unhide the language button every time, as it could've been previously + // hidden by a language change. + uiLanguageButton.hidden = false; + + if (languageCode == this.prospectiveUiLanguageCode_) { + uiLanguageMessage.textContent = + loadTimeData.getString('isDisplayedInThisLanguage'); + showMutuallyExclusiveNodes( + [uiLanguageButton, uiLanguageMessage, uiLanguageNotification], 1); + } else if (languageCode in loadTimeData.getValue('uiLanguageCodeSet')) { + if (cr.isChromeOS && UIAccountTweaks.loggedInAsGuest()) { + // In the guest mode for ChromeOS, changing UI language does not make + // sense because it does not take effect after browser restart. + uiLanguageButton.hidden = true; + uiLanguageMessage.hidden = true; + } else { + uiLanguageButton.textContent = + loadTimeData.getString('displayInThisLanguage'); + showMutuallyExclusiveNodes( + [uiLanguageButton, uiLanguageMessage, uiLanguageNotification], 0); + uiLanguageButton.onclick = function(e) { + chrome.send('uiLanguageChange', [languageCode]); + }; + } + } else { + uiLanguageMessage.textContent = + loadTimeData.getString('cannotBeDisplayedInThisLanguage'); + showMutuallyExclusiveNodes( + [uiLanguageButton, uiLanguageMessage, uiLanguageNotification], 1); + } + }, + + /** + * Updates the spell check language button. + * @param {string} languageCode Language code (ex. "fr"). + * @private + */ + updateSpellCheckLanguageButton_: function(languageCode) { + var spellCheckLanguageSection = $('language-options-spellcheck'); + var spellCheckLanguageButton = + $('language-options-spell-check-language-button'); + var spellCheckLanguageMessage = + $('language-options-spell-check-language-message'); + var dictionaryDownloadInProgress = + $('language-options-dictionary-downloading-message'); + var dictionaryDownloadFailed = + $('language-options-dictionary-download-failed-message'); + var dictionaryDownloadFailHelp = + $('language-options-dictionary-download-fail-help-message'); + spellCheckLanguageSection.hidden = false; + spellCheckLanguageMessage.hidden = true; + spellCheckLanguageButton.hidden = true; + dictionaryDownloadInProgress.hidden = true; + dictionaryDownloadFailed.hidden = true; + dictionaryDownloadFailHelp.hidden = true; + + if (languageCode == this.spellCheckDictionary_) { + if (!(languageCode in this.spellcheckDictionaryDownloadStatus_)) { + spellCheckLanguageMessage.textContent = + loadTimeData.getString('isUsedForSpellChecking'); + showMutuallyExclusiveNodes( + [spellCheckLanguageButton, spellCheckLanguageMessage], 1); + } else if (this.spellcheckDictionaryDownloadStatus_[languageCode] == + DOWNLOAD_STATUS.IN_PROGRESS) { + dictionaryDownloadInProgress.hidden = false; + } else if (this.spellcheckDictionaryDownloadStatus_[languageCode] == + DOWNLOAD_STATUS.FAILED) { + spellCheckLanguageSection.hidden = true; + dictionaryDownloadFailed.hidden = false; + if (this.spellcheckDictionaryDownloadFailures_ > 1) + dictionaryDownloadFailHelp.hidden = false; + } + } else if (languageCode in + loadTimeData.getValue('spellCheckLanguageCodeSet')) { + spellCheckLanguageButton.textContent = + loadTimeData.getString('useThisForSpellChecking'); + showMutuallyExclusiveNodes( + [spellCheckLanguageButton, spellCheckLanguageMessage], 0); + spellCheckLanguageButton.languageCode = languageCode; + } else if (!languageCode) { + spellCheckLanguageButton.hidden = true; + spellCheckLanguageMessage.hidden = true; + } else { + spellCheckLanguageMessage.textContent = + loadTimeData.getString('cannotBeUsedForSpellChecking'); + showMutuallyExclusiveNodes( + [spellCheckLanguageButton, spellCheckLanguageMessage], 1); + } + }, + + /** + * Updates the checkbox for stopping translation. + * @param {string} languageCode Language code (ex. "fr"). + * @private + */ + updateOfferToTranslateCheckbox_: function(languageCode) { + var div = $('language-options-offer-to-translate'); + + // Translation server supports Chinese (Transitional) and Chinese + // (Simplified) but not 'general' Chinese. To avoid ambiguity, we don't + // show this preference when general Chinese is selected. + if (languageCode != 'zh') { + div.hidden = false; + } else { + div.hidden = true; + return; + } + + var offerToTranslate = div.querySelector('div'); + var cannotTranslate = $('cannot-translate-in-this-language'); + var nodes = [offerToTranslate, cannotTranslate]; + + var convertedLangCode = this.convertLangCodeForTranslation_(languageCode); + if (this.translateSupportedLanguages_.indexOf(convertedLangCode) != -1) { + showMutuallyExclusiveNodes(nodes, 0); + } else { + showMutuallyExclusiveNodes(nodes, 1); + return; + } + + var checkbox = $('offer-to-translate-in-this-language'); + + if (!this.enableTranslate_) { + checkbox.disabled = true; + checkbox.checked = false; + return; + } + + // If the language corresponds to the default target language (in most + // cases, the user's locale language), "Offer to translate" checkbox + // should be always unchecked. + var defaultTargetLanguage = + loadTimeData.getString('defaultTargetLanguage'); + if (convertedLangCode == defaultTargetLanguage) { + checkbox.disabled = true; + checkbox.checked = false; + return; + } + + checkbox.disabled = false; + + var blockedLanguages = this.translateBlockedLanguages_; + var checked = blockedLanguages.indexOf(convertedLangCode) == -1; + checkbox.checked = checked; + }, + + /** + * Updates the input method list. + * @param {string} languageCode Language code (ex. "fr"). + * @private + */ + updateInputMethodList_: function(languageCode) { + // Give one of the checkboxes or buttons focus, if it's specified in the + // URL hash (ex. focus=mozc). Used for automated testing. + var focusInputMethodId = -1; + var match = document.location.hash.match(/\bfocus=([\w:-]+)\b/); + if (match) { + focusInputMethodId = match[1]; + } + // Change the visibility of the input method list. Input methods that + // matches |languageCode| will become visible. + var inputMethodList = $('language-options-input-method-list'); + var methods = inputMethodList.querySelectorAll('.input-method'); + for (var i = 0; i < methods.length; i++) { + var method = methods[i]; + if (languageCode in method.languageCodeSet) { + method.hidden = false; + var input = method.querySelector('input'); + // Give it focus if the ID matches. + if (input.inputMethodId == focusInputMethodId) { + input.focus(); + } + } else { + method.hidden = true; + } + } + + $('language-options-input-method-none').hidden = + (languageCode in this.languageCodeToInputMethodIdsMap_); + + if (focusInputMethodId == 'add') { + $('language-options-add-button').focus(); + } + }, + + /** + * Updates the language list in the add language overlay. + * @param {string} languageCode Language code (ex. "fr"). + * @private + */ + updateLanguageListInAddLanguageOverlay_: function(languageCode) { + // Change the visibility of the language list in the add language + // overlay. Languages that are already active will become invisible, + // so that users don't add the same language twice. + var languageOptionsList = $('language-options-list'); + var languageCodes = languageOptionsList.getLanguageCodes(); + var languageCodeSet = {}; + for (var i = 0; i < languageCodes.length; i++) { + languageCodeSet[languageCodes[i]] = true; + } + + var addLanguageList = $('add-language-overlay-language-list'); + var options = addLanguageList.querySelectorAll('option'); + assert(options.length > 0); + var selectedFirstItem = false; + for (var i = 0; i < options.length; i++) { + var option = options[i]; + option.hidden = option.value in languageCodeSet; + if (!option.hidden && !selectedFirstItem) { + // Select first visible item, otherwise previously selected hidden + // item will be selected by default at the next time. + option.selected = true; + selectedFirstItem = true; + } + } + }, + + /** + * Handles preloadEnginesPref change. + * @param {Event} e Change event. + * @private + */ + handlePreloadEnginesPrefChange_: function(e) { + var value = e.value.value; + this.preloadEngines_ = this.filterBadPreloadEngines_(value.split(',')); + this.updateCheckboxesFromPreloadEngines_(); + $('language-options-list').updateDeletable(); + }, + + /** + * Handles enabledExtensionImePref change. + * @param {Event} e Change event. + * @private + */ + handleEnabledExtensionsPrefChange_: function(e) { + var value = e.value.value; + this.enabledExtensionImes_ = value.split(','); + this.updateCheckboxesFromEnabledExtensions_(); + }, + + /** + * Handles offer-to-translate checkbox's click event. + * @param {Event} e Click event. + * @private + */ + handleOfferToTranslateCheckboxClick_: function(e) { + var checkbox = e.target; + var checked = checkbox.checked; + + var languageOptionsList = $('language-options-list'); + var selectedLanguageCode = languageOptionsList.getSelectedLanguageCode(); + + if (checked) + this.removeBlockedLanguage_(selectedLanguageCode); + else + this.addBlockedLanguage_(selectedLanguageCode); + }, + + /** + * Handles input method checkbox's click event. + * @param {Event} e Click event. + * @private + */ + handleCheckboxClick_: function(e) { + var checkbox = e.target; + + if (checkbox.inputMethodId.match(/^_ext_ime_/)) { + this.updateEnabledExtensionsFromCheckboxes_(); + this.saveEnabledExtensionPref_(); + return; + } + if (this.preloadEngines_.length == 1 && !checkbox.checked) { + // Don't allow disabling the last input method. + this.showNotification_( + loadTimeData.getString('pleaseAddAnotherInputMethod'), + loadTimeData.getString('okButton')); + checkbox.checked = true; + return; + } + if (checkbox.checked) { + chrome.send('inputMethodEnable', [checkbox.inputMethodId]); + } else { + chrome.send('inputMethodDisable', [checkbox.inputMethodId]); + } + this.updatePreloadEnginesFromCheckboxes_(); + this.preloadEngines_ = this.sortPreloadEngines_(this.preloadEngines_); + this.savePreloadEnginesPref_(); + }, + + handleAddLanguageOkButtonClick_: function() { + var languagesSelect = $('add-language-overlay-language-list'); + var selectedIndex = languagesSelect.selectedIndex; + if (selectedIndex >= 0) { + var selection = languagesSelect.options[selectedIndex]; + var langCode = String(selection.value); + $('language-options-list').addLanguage(langCode); + this.addBlockedLanguage_(langCode); + OptionsPage.closeOverlay(); + } + }, + + /** + * Checks if languageCode is deletable or not. + * @param {string} languageCode the languageCode to check for deletability. + */ + languageIsDeletable: function(languageCode) { + // Don't allow removing the language if it's a UI language. + if (languageCode == this.prospectiveUiLanguageCode_) + return false; + return (!cr.isChromeOS || + this.canDeleteLanguage_(languageCode)); + }, + + /** + * Handles browse.enable_spellchecking change. + * @param {Event} e Change event. + * @private + */ + updateEnableSpellCheck_: function() { + var value = !$('enable-spell-check').checked; + $('language-options-spell-check-language-button').disabled = value; + if (!cr.IsMac) + $('edit-dictionary-button').hidden = value; + }, + + /** + * Handles translateBlockedLanguagesPref change. + * @param {Event} e Change event. + * @private + */ + handleTranslateBlockedLanguagesPrefChange_: function(e) { + this.translateBlockedLanguages_ = e.value.value; + this.updateOfferToTranslateCheckbox_( + $('language-options-list').getSelectedLanguageCode()); + }, + + /** + * Handles spellCheckDictionaryPref change. + * @param {Event} e Change event. + * @private + */ + handleSpellCheckDictionaryPrefChange_: function(e) { + var languageCode = e.value.value; + this.spellCheckDictionary_ = languageCode; + if (!cr.isMac) { + this.updateSpellCheckLanguageButton_( + $('language-options-list').getSelectedLanguageCode()); + } + }, + + /** + * Handles translate.enabled change. + * @param {Event} e Change event. + * @private + */ + handleEnableTranslatePrefChange_: function(e) { + var enabled = e.value.value; + this.enableTranslate_ = enabled; + this.updateOfferToTranslateCheckbox_( + $('language-options-list').getSelectedLanguageCode()); + }, + + /** + * Handles spellCheckLanguageButton click. + * @param {Event} e Click event. + * @private + */ + handleSpellCheckLanguageButtonClick_: function(e) { + var languageCode = e.target.languageCode; + // Save the preference. + Preferences.setStringPref(SPELL_CHECK_DICTIONARY_PREF, + languageCode, true); + chrome.send('spellCheckLanguageChange', [languageCode]); + }, + + /** + * Checks whether it's possible to remove the language specified by + * languageCode and returns true if possible. This function returns false + * if the removal causes the number of preload engines to be zero. + * + * @param {string} languageCode Language code (ex. "fr"). + * @return {boolean} Returns true on success. + * @private + */ + canDeleteLanguage_: function(languageCode) { + // First create the set of engines to be removed from input methods + // associated with the language code. + var enginesToBeRemovedSet = {}; + var inputMethodIds = this.languageCodeToInputMethodIdsMap_[languageCode]; + + // If this language doesn't have any input methods, it can be deleted. + if (!inputMethodIds) + return true; + + for (var i = 0; i < inputMethodIds.length; i++) { + enginesToBeRemovedSet[inputMethodIds[i]] = true; + } + + // Then eliminate engines that are also used for other active languages. + // For instance, if "xkb:us::eng" is used for both English and Filipino. + var languageCodes = $('language-options-list').getLanguageCodes(); + for (var i = 0; i < languageCodes.length; i++) { + // Skip the target language code. + if (languageCodes[i] == languageCode) { + continue; + } + // Check if input methods used in this language are included in + // enginesToBeRemovedSet. If so, eliminate these from the set, so + // we don't remove this time. + var inputMethodIdsForAnotherLanguage = + this.languageCodeToInputMethodIdsMap_[languageCodes[i]]; + if (!inputMethodIdsForAnotherLanguage) + continue; + + for (var j = 0; j < inputMethodIdsForAnotherLanguage.length; j++) { + var inputMethodId = inputMethodIdsForAnotherLanguage[j]; + if (inputMethodId in enginesToBeRemovedSet) { + delete enginesToBeRemovedSet[inputMethodId]; + } + } + } + + // Update the preload engine list with the to-be-removed set. + var newPreloadEngines = []; + for (var i = 0; i < this.preloadEngines_.length; i++) { + if (!(this.preloadEngines_[i] in enginesToBeRemovedSet)) { + newPreloadEngines.push(this.preloadEngines_[i]); + } + } + // Don't allow this operation if it causes the number of preload + // engines to be zero. + return (newPreloadEngines.length > 0); + }, + + /** + * Saves the enabled extension preference. + * @private + */ + saveEnabledExtensionPref_: function() { + Preferences.setStringPref(ENABLED_EXTENSION_IME_PREF, + this.enabledExtensionImes_.join(','), true); + }, + + /** + * Updates the checkboxes in the input method list from the enabled + * extensions preference. + * @private + */ + updateCheckboxesFromEnabledExtensions_: function() { + // Convert the list into a dictonary for simpler lookup. + var dictionary = {}; + for (var i = 0; i < this.enabledExtensionImes_.length; i++) + dictionary[this.enabledExtensionImes_[i]] = true; + + var inputMethodList = $('language-options-input-method-list'); + var checkboxes = inputMethodList.querySelectorAll('input'); + for (var i = 0; i < checkboxes.length; i++) { + if (checkboxes[i].inputMethodId.match(/^_ext_ime_/)) + checkboxes[i].checked = (checkboxes[i].inputMethodId in dictionary); + } + var configureButtons = inputMethodList.querySelectorAll('button'); + for (var i = 0; i < configureButtons.length; i++) { + if (configureButtons[i].inputMethodId.match(/^_ext_ime_/)) { + configureButtons[i].hidden = + !(configureButtons[i].inputMethodId in dictionary); + } + } + }, + + /** + * Updates the enabled extensions preference from the checkboxes in the + * input method list. + * @private + */ + updateEnabledExtensionsFromCheckboxes_: function() { + this.enabledExtensionImes_ = []; + var inputMethodList = $('language-options-input-method-list'); + var checkboxes = inputMethodList.querySelectorAll('input'); + for (var i = 0; i < checkboxes.length; i++) { + if (checkboxes[i].inputMethodId.match(/^_ext_ime_/)) { + if (checkboxes[i].checked) + this.enabledExtensionImes_.push(checkboxes[i].inputMethodId); + } + } + }, + + /** + * Saves the preload engines preference. + * @private + */ + savePreloadEnginesPref_: function() { + Preferences.setStringPref(PRELOAD_ENGINES_PREF, + this.preloadEngines_.join(','), true); + }, + + /** + * Updates the checkboxes in the input method list from the preload + * engines preference. + * @private + */ + updateCheckboxesFromPreloadEngines_: function() { + // Convert the list into a dictonary for simpler lookup. + var dictionary = {}; + for (var i = 0; i < this.preloadEngines_.length; i++) { + dictionary[this.preloadEngines_[i]] = true; + } + + var inputMethodList = $('language-options-input-method-list'); + var checkboxes = inputMethodList.querySelectorAll('input'); + for (var i = 0; i < checkboxes.length; i++) { + if (!checkboxes[i].inputMethodId.match(/^_ext_ime_/)) + checkboxes[i].checked = (checkboxes[i].inputMethodId in dictionary); + } + var configureButtons = inputMethodList.querySelectorAll('button'); + for (var i = 0; i < configureButtons.length; i++) { + if (!configureButtons[i].inputMethodId.match(/^_ext_ime_/)) { + configureButtons[i].hidden = + !(configureButtons[i].inputMethodId in dictionary); + } + } + }, + + /** + * Updates the preload engines preference from the checkboxes in the + * input method list. + * @private + */ + updatePreloadEnginesFromCheckboxes_: function() { + this.preloadEngines_ = []; + var inputMethodList = $('language-options-input-method-list'); + var checkboxes = inputMethodList.querySelectorAll('input'); + for (var i = 0; i < checkboxes.length; i++) { + if (!checkboxes[i].inputMethodId.match(/^_ext_ime_/)) { + if (checkboxes[i].checked) + this.preloadEngines_.push(checkboxes[i].inputMethodId); + } + } + var languageOptionsList = $('language-options-list'); + languageOptionsList.updateDeletable(); + }, + + /** + * Filters bad preload engines in case bad preload engines are + * stored in the preference. Removes duplicates as well. + * @param {Array} preloadEngines List of preload engines. + * @private + */ + filterBadPreloadEngines_: function(preloadEngines) { + // Convert the list into a dictonary for simpler lookup. + var dictionary = {}; + var list = loadTimeData.getValue('inputMethodList'); + for (var i = 0; i < list.length; i++) { + dictionary[list[i].id] = true; + } + + var enabledPreloadEngines = []; + var seen = {}; + for (var i = 0; i < preloadEngines.length; i++) { + // Check if the preload engine is present in the + // dictionary, and not duplicate. Otherwise, skip it. + // Component Extension IME should be handled same as preloadEngines and + // "_comp_" is the special prefix of its ID. + if ((preloadEngines[i] in dictionary && !(preloadEngines[i] in seen)) || + /^_comp_/.test(preloadEngines[i])) { + enabledPreloadEngines.push(preloadEngines[i]); + seen[preloadEngines[i]] = true; + } + } + return enabledPreloadEngines; + }, + + // TODO(kochi): This is an adapted copy from new_tab.js. + // If this will go as final UI, refactor this to share the component with + // new new tab page. + /** + * Shows notification + * @private + */ + notificationTimeout_: null, + showNotification_: function(text, actionText, opt_delay) { + var notificationElement = $('notification'); + var actionLink = notificationElement.querySelector('.link-color'); + var delay = opt_delay || 10000; + + function show() { + window.clearTimeout(this.notificationTimeout_); + notificationElement.classList.add('show'); + document.body.classList.add('notification-shown'); + } + + function hide() { + window.clearTimeout(this.notificationTimeout_); + notificationElement.classList.remove('show'); + document.body.classList.remove('notification-shown'); + // Prevent tabbing to the hidden link. + actionLink.tabIndex = -1; + // Setting tabIndex to -1 only prevents future tabbing to it. If, + // however, the user switches window or a tab and then moves back to + // this tab the element may gain focus. We therefore make sure that we + // blur the element so that the element focus is not restored when + // coming back to this window. + actionLink.blur(); + } + + function delayedHide() { + this.notificationTimeout_ = window.setTimeout(hide, delay); + } + + notificationElement.firstElementChild.textContent = text; + actionLink.textContent = actionText; + + actionLink.onclick = hide; + actionLink.onkeydown = function(e) { + if (e.keyIdentifier == 'Enter') { + hide(); + } + }; + notificationElement.onmouseover = show; + notificationElement.onmouseout = delayedHide; + actionLink.onfocus = show; + actionLink.onblur = delayedHide; + // Enable tabbing to the link now that it is shown. + actionLink.tabIndex = 0; + + show(); + delayedHide(); + }, + + onDictionaryDownloadBegin_: function(languageCode) { + this.spellcheckDictionaryDownloadStatus_[languageCode] = + DOWNLOAD_STATUS.IN_PROGRESS; + if (!cr.isMac && + languageCode == + $('language-options-list').getSelectedLanguageCode()) { + this.updateSpellCheckLanguageButton_(languageCode); + } + }, + + onDictionaryDownloadSuccess_: function(languageCode) { + delete this.spellcheckDictionaryDownloadStatus_[languageCode]; + this.spellcheckDictionaryDownloadFailures_ = 0; + if (!cr.isMac && + languageCode == + $('language-options-list').getSelectedLanguageCode()) { + this.updateSpellCheckLanguageButton_(languageCode); + } + }, + + onDictionaryDownloadFailure_: function(languageCode) { + this.spellcheckDictionaryDownloadStatus_[languageCode] = + DOWNLOAD_STATUS.FAILED; + this.spellcheckDictionaryDownloadFailures_++; + if (!cr.isMac && + languageCode == + $('language-options-list').getSelectedLanguageCode()) { + this.updateSpellCheckLanguageButton_(languageCode); + } + }, + + /* + * Converts the language code for Translation. There are some differences + * between the language set for Translation and that for Accept-Language. + * @param {string} languageCode The language code like 'fr'. + * @return {string} The converted language code. + * @private + */ + convertLangCodeForTranslation_: function(languageCode) { + var tokens = languageCode.split('-'); + var main = tokens[0]; + + // See also: chrome/renderer/translate/translate_helper.cc. + var synonyms = { + 'nb': 'no', + 'he': 'iw', + 'jv': 'jw', + 'fil': 'tl', + }; + + if (main in synonyms) { + return synonyms[main]; + } else if (main == 'zh') { + // In Translation, general Chinese is not used, and the sub code is + // necessary as a language code for Translate server. + return languageCode; + } + + return main; + }, + }; + + /** + * Shows the node at |index| in |nodes|, hides all others. + * @param {Array<HTMLElement>} nodes The nodes to be shown or hidden. + * @param {number} index The index of |nodes| to show. + */ + function showMutuallyExclusiveNodes(nodes, index) { + assert(index >= 0 && index < nodes.length); + for (var i = 0; i < nodes.length; ++i) { + assert(nodes[i] instanceof HTMLElement); // TODO(dbeam): Ignore null? + nodes[i].hidden = i != index; + } + } + + /** + * Chrome callback for when the UI language preference is saved. + * @param {string} languageCode The newly selected language to use. + */ + LanguageOptions.uiLanguageSaved = function(languageCode) { + this.prospectiveUiLanguageCode_ = languageCode; + + // If the user is no longer on the same language code, ignore. + if ($('language-options-list').getSelectedLanguageCode() != languageCode) + return; + + // Special case for when a user changes to a different language, and changes + // back to the same language without having restarted Chrome or logged + // in/out of ChromeOS. + if (languageCode == loadTimeData.getString('currentUiLanguageCode')) { + LanguageOptions.getInstance().currentLocaleWasReselected(); + return; + } + + // Otherwise, show a notification telling the user that their changes will + // only take effect after restart. + showMutuallyExclusiveNodes([$('language-options-ui-language-button'), + $('language-options-ui-notification-bar')], 1); + }; + + LanguageOptions.onDictionaryDownloadBegin = function(languageCode) { + LanguageOptions.getInstance().onDictionaryDownloadBegin_(languageCode); + }; + + LanguageOptions.onDictionaryDownloadSuccess = function(languageCode) { + LanguageOptions.getInstance().onDictionaryDownloadSuccess_(languageCode); + }; + + LanguageOptions.onDictionaryDownloadFailure = function(languageCode) { + LanguageOptions.getInstance().onDictionaryDownloadFailure_(languageCode); + }; + + LanguageOptions.onComponentManagerInitialized = function(componentImes) { + LanguageOptions.getInstance().appendComponentExtensionIme_(componentImes); + }; + + // Export + return { + LanguageOptions: LanguageOptions + }; +}); diff --git a/chromium/chrome/browser/resources/options/manage_profile_overlay.css b/chromium/chrome/browser/resources/options/manage_profile_overlay.css new file mode 100644 index 00000000000..760030db3d2 --- /dev/null +++ b/chromium/chrome/browser/resources/options/manage_profile_overlay.css @@ -0,0 +1,130 @@ +/* Copyright (c) 2012 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ + +#manage-profile-overlay { + width: 612px; +} + +.profile-icon-grid-item { + height: 31px; + margin: 2px 4px; + padding: 4px; + width: 38px; +} + +.profile-icon { + height: 31px; + width: 38px; +} + +#create-profile-name-input-container, +#manage-profile-name-input-container { + margin-top: 5px; +} + +#create-profile-name, +#manage-profile-name { + margin-left: 10px; +} + +#create-profile-name:invalid, +#manage-profile-name:invalid { + background-color: pink; +} + +#create-profile-error-bubble, +#manage-profile-error-bubble { + -webkit-transition: max-height 200ms, padding 200ms; + background-color: rgb(238, 185, 57); + border-radius: 4px; + font-weight: bold; + margin-left: auto; + margin-right: auto; + max-height: 50px; + overflow: hidden; + padding: 1px 10px; + text-align: center; + width: 80%; +} + +#create-profile-error-bubble[hidden], +#manage-profile-error-bubble[hidden] { + display: block !important; + max-height: 0; + padding: 0 10px; +} + +#create-profile-icon-grid, +#manage-profile-icon-grid { + background-color: rgba(255, 255, 255, 0.75); + padding: 2px; +} + +:-webkit-any(#create-profile-content, #manage-profile-content) > + :not(:last-child) { + margin-bottom: 10px; +} + +:-webkit-any(#create-profile-content, #manage-profile-content) > + :not(:first-child) { + margin-top: 10px; +} + +:-webkit-any(#create-profile-content, #manage-profile-content) > + .name-input-container { + margin-top: 5px; +} + +:-webkit-any(#create-profile-content, #manage-profile-content) > + .name-label-container { + margin-bottom: 5px; +} + +#create-profile-content { + padding-bottom: 0; +} + +.action-area-shortcut-container { + -webkit-box-flex: 1; +} + +/* Proper spacing for the buttons. */ +#remove-shortcut-button, +#add-shortcut-button { + -webkit-margin-end: 10px; +} + +#delete-managed-profile-addendum { + -webkit-padding-start: 48px; + margin-top: 10px; +} + +html[dir='ltr'] #delete-profile-icon { + float: left; + margin-right: 10px; +} + +html[dir='rtl'] #delete-profile-icon { + float: right; + margin-left: 10px; +} + +#create-profile-managed-not-signed-in { + color: #999; +} + +#create-profile-managed-not-signed-in-label, +#create-profile-managed-account-details-out-of-date-label { + white-space: pre-wrap; + word-wrap: break-word; +} + +#create-profile-managed-content-area { + padding-top: 0; +} + +#import-existing-managed-user-link { + left: 17px; + position: absolute; +} diff --git a/chromium/chrome/browser/resources/options/manage_profile_overlay.html b/chromium/chrome/browser/resources/options/manage_profile_overlay.html new file mode 100644 index 00000000000..42ccfb21cae --- /dev/null +++ b/chromium/chrome/browser/resources/options/manage_profile_overlay.html @@ -0,0 +1,124 @@ +<div id="manage-profile-overlay" class="page" hidden> + <div class="close-button"></div> + <!-- Dialog for managing profiles. --> + <div id="manage-profile-overlay-manage" hidden> + <h1 i18n-content="manageProfile"></h1> + <div id="manage-profile-content" class="content-area"> + <div id="manage-profile-icon-label" + i18n-content="manageProfilesIconLabel"> + </div> + <grid id="manage-profile-icon-grid"></grid> + <div id="manage-profile-name-input-container"> + <label> + <span id="manage-profile-name-label" for="manage-profile-name" + i18n-content="manageProfilesNameLabel"> + </span> + <input id="manage-profile-name" type="text" required> + </label> + </div> + <div id="manage-profile-error-bubble" hidden></div> + </div> + <div class="action-area"> + <div class="action-area-shortcut-container"> + <button id="remove-shortcut-button" + i18n-content="removeProfileShortcutButton" hidden> + </button> + <button id="add-shortcut-button" + i18n-content="createProfileShortcutButton" hidden> + </button> + </div> + <div class="button-strip"> + <button id="manage-profile-cancel" i18n-content="cancel"></button> + <button id="manage-profile-ok" i18n-content="ok" + class="default-button"></button> + </div> + </div> + </div> + <!-- Dialog for deleting profiles. --> + <div id="manage-profile-overlay-delete" hidden> + <h1 i18n-content="deleteProfileTitle"></h1> + <div class="content-area"> + <div id="delete-profile-message"> + <img id="delete-profile-icon" class="profile-icon"> + <div id="delete-profile-text"></div> + </div> + <div id="delete-managed-profile-addendum" + i18n-values=".innerHTML:deleteManagedProfileAddendum" hidden> + </div> + </div> + <div class="action-area button-strip"> + <button id="delete-profile-cancel" i18n-content="cancel"></button> + <button id="delete-profile-ok" class="default-button" + i18n-content="deleteProfileOK"></button> + </div> + </div> + <!-- Dialog for creating profiles. --> + <div id="manage-profile-overlay-create" hidden> + <h1 i18n-content="createProfileTitle"></h1> + <div id="create-profile-content" class="content-area"> + <div id="create-profile-instructions"></div> + <grid id="create-profile-icon-grid"></grid> + <div id="create-profile-name-input-container"> + <label> + <span id="create-profile-name-label" for="create-profile-name" + i18n-content="manageProfilesNameLabel"> + </span> + <input id="create-profile-name" type="text" required> + </label> + </div> + <div id="create-profile-error-bubble" hidden></div> + </div> + <div id="create-profile-managed-content-area" class="content-area"> + <div id="create-shortcut-container" class="checkbox" hidden> + <label> + <input id="create-shortcut" type="checkbox"> + <span for="create-shortcut" + i18n-content="createProfileShortcutCheckbox"> + </span> + </label> + </div> + <div id="create-profile-managed-container" class="checkbox"> + <label> + <input id="create-profile-managed" type="checkbox"> + <span id="create-profile-managed-signed-in"> + <span id="create-profile-managed-signed-in-label"></span> + <span id="create-profile-managed-account-details-out-of-date-label" + hidden> + </span> + <button id="create-profile-managed-signed-in-learn-more-link" + class="link-button" i18n-content="learnMore"> + </button> + <button id="create-profile-managed-sign-in-again-link" + class="link-button" + i18n-content="manageProfilesManagedSignInAgainLink" hidden> + </button> + </span> + <span id="create-profile-managed-not-signed-in" hidden> + <span id="create-profile-managed-not-signed-in-label" + i18n-content="manageProfilesManagedNotSignedInLabel"> + </span> + <button id="create-profile-managed-not-signed-in-link" + class="link-button" + i18n-content="manageProfilesManagedNotSignedInLink"> + </button> + </span> + </label> + <span id="create-profile-managed-indicator" + class="bubble-button controlled-setting-indicator"> + </span> + </div> + </div> + <div class="action-area"> + <div id="create-profile-throbber" class="throbber"></div> + <button id="import-existing-managed-user-link" class="link-button" + i18n-content="importExistingManagedUserLink"> + </button> + <div class="button-strip"> + <button id="create-profile-cancel" i18n-content="cancel"></button> + <button id="create-profile-ok" i18n-content="createProfileConfirm" + class="default-button"> + </button> + </div> + </div> + </div> +</div> diff --git a/chromium/chrome/browser/resources/options/manage_profile_overlay.js b/chromium/chrome/browser/resources/options/manage_profile_overlay.js new file mode 100644 index 00000000000..2d6e64c717f --- /dev/null +++ b/chromium/chrome/browser/resources/options/manage_profile_overlay.js @@ -0,0 +1,704 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + var OptionsPage = options.OptionsPage; + var ArrayDataModel = cr.ui.ArrayDataModel; + + /** + * ManageProfileOverlay class + * Encapsulated handling of the 'Manage profile...' overlay page. + * @constructor + * @class + */ + function ManageProfileOverlay() { + OptionsPage.call(this, 'manageProfile', + loadTimeData.getString('manageProfileTabTitle'), + 'manage-profile-overlay'); + }; + + cr.addSingletonGetter(ManageProfileOverlay); + + ManageProfileOverlay.prototype = { + // Inherit from OptionsPage. + __proto__: OptionsPage.prototype, + + // Info about the currently managed/deleted profile. + profileInfo_: null, + + // An object containing all known profile names. + profileNames_: {}, + + // The currently selected icon in the icon grid. + iconGridSelectedURL_: null, + + /** + * Initialize the page. + */ + initializePage: function() { + // Call base class implementation to start preference initialization. + OptionsPage.prototype.initializePage.call(this); + + var self = this; + options.ProfilesIconGrid.decorate($('manage-profile-icon-grid')); + options.ProfilesIconGrid.decorate($('create-profile-icon-grid')); + self.registerCommonEventHandlers_('create', + self.submitCreateProfile_.bind(self)); + self.registerCommonEventHandlers_('manage', + self.submitManageChanges_.bind(self)); + + // Override the create-profile-ok and create-* keydown handlers, to avoid + // closing the overlay until we finish creating the profile. + $('create-profile-ok').onclick = function(event) { + self.submitCreateProfile_(); + }; + + $('create-profile-cancel').onclick = function(event) { + CreateProfileOverlay.cancelCreateProfile(); + }; + + $('import-existing-managed-user-link').hidden = + !loadTimeData.getBoolean('allowCreateExistingManagedUsers'); + + $('manage-profile-cancel').onclick = + $('delete-profile-cancel').onclick = function(event) { + OptionsPage.closeOverlay(); + }; + $('delete-profile-ok').onclick = function(event) { + OptionsPage.closeOverlay(); + if (BrowserOptions.getCurrentProfile().isManaged) + return; + chrome.send('deleteProfile', [self.profileInfo_.filePath]); + }; + $('add-shortcut-button').onclick = function(event) { + chrome.send('addProfileShortcut', [self.profileInfo_.filePath]); + }; + $('remove-shortcut-button').onclick = function(event) { + chrome.send('removeProfileShortcut', [self.profileInfo_.filePath]); + }; + + $('create-profile-managed-signed-in-learn-more-link').onclick = + function(event) { + OptionsPage.navigateToPage('managedUserLearnMore'); + return false; + }; + + $('create-profile-managed-not-signed-in-link').onclick = function(event) { + // The signin process will open an overlay to configure sync, which + // would replace this overlay. It's smoother to close this one now. + // TODO(pamg): Move the sync-setup overlay to a higher layer so this one + // can stay open under it, after making sure that doesn't break anything + // else. + OptionsPage.closeOverlay(); + SyncSetupOverlay.startSignIn(); + }; + + $('create-profile-managed-sign-in-again-link').onclick = function(event) { + OptionsPage.closeOverlay(); + SyncSetupOverlay.showSetupUI(); + }; + + $('import-existing-managed-user-link').onclick = function(event) { + OptionsPage.closeOverlay(); + OptionsPage.navigateToPage('managedUserImport'); + }; + }, + + /** @override */ + didShowPage: function() { + chrome.send('requestDefaultProfileIcons'); + + // Just ignore the manage profile dialog on Chrome OS, they use /accounts. + if (!cr.isChromeOS && window.location.pathname == '/manageProfile') + ManageProfileOverlay.getInstance().prepareForManageDialog_(); + + // When editing a profile, initially hide the "add shortcut" and + // "remove shortcut" buttons and ask the handler which to show. It will + // call |receiveHasProfileShortcuts|, which will show the appropriate one. + $('remove-shortcut-button').hidden = true; + $('add-shortcut-button').hidden = true; + + if (loadTimeData.getBoolean('profileShortcutsEnabled')) { + var profileInfo = ManageProfileOverlay.getInstance().profileInfo_; + chrome.send('requestHasProfileShortcuts', [profileInfo.filePath]); + } + + var manageNameField = $('manage-profile-name'); + // Supervised users cannot edit their names. + if (manageNameField.disabled) + $('manage-profile-ok').focus(); + else + manageNameField.focus(); + }, + + /** + * Registers event handlers that are common between create and manage modes. + * @param {string} mode A label that specifies the type of dialog + * box which is currently being viewed (i.e. 'create' or + * 'manage'). + * @param {function()} submitFunction The function that should be called + * when the user chooses to submit (e.g. by clicking the OK button). + * @private + */ + registerCommonEventHandlers_: function(mode, submitFunction) { + var self = this; + $(mode + '-profile-icon-grid').addEventListener('change', function(e) { + self.onIconGridSelectionChanged_(mode); + }); + $(mode + '-profile-name').oninput = function(event) { + self.onNameChanged_(event, mode); + }; + $(mode + '-profile-ok').onclick = function(event) { + OptionsPage.closeOverlay(); + submitFunction(); + }; + }, + + /** + * Set the profile info used in the dialog. + * @param {Object} profileInfo An object of the form: + * profileInfo = { + * name: "Profile Name", + * iconURL: "chrome://path/to/icon/image", + * filePath: "/path/to/profile/data/on/disk", + * isCurrentProfile: false, + * isManaged: false + * }; + * @param {string} mode A label that specifies the type of dialog + * box which is currently being viewed (i.e. 'create' or + * 'manage'). + * @private + */ + setProfileInfo_: function(profileInfo, mode) { + this.iconGridSelectedURL_ = profileInfo.iconURL; + this.profileInfo_ = profileInfo; + $(mode + '-profile-name').value = profileInfo.name; + $(mode + '-profile-icon-grid').selectedItem = profileInfo.iconURL; + }, + + /** + * Sets the name of the currently edited profile. + * @private + */ + setProfileName_: function(name) { + if (this.profileInfo_) + this.profileInfo_.name = name; + $('manage-profile-name').value = name; + }, + + /** + * Set an array of default icon URLs. These will be added to the grid that + * the user will use to choose their profile icon. + * @param {Array.<string>} iconURLs An array of icon URLs. + * @private + */ + receiveDefaultProfileIcons_: function(iconGrid, iconURLs) { + $(iconGrid).dataModel = new ArrayDataModel(iconURLs); + + if (this.profileInfo_) + $(iconGrid).selectedItem = this.profileInfo_.iconURL; + + var grid = $(iconGrid); + // Recalculate the measured item size. + grid.measured_ = null; + grid.columns = 0; + grid.redraw(); + }, + + /** + * Callback to set the initial values when creating a new profile. + * @param {Object} profileInfo An object of the form: + * profileInfo = { + * name: "Profile Name", + * iconURL: "chrome://path/to/icon/image", + * }; + * @private + */ + receiveNewProfileDefaults_: function(profileInfo) { + ManageProfileOverlay.setProfileInfo(profileInfo, 'create'); + $('create-profile-name-label').hidden = false; + $('create-profile-name').hidden = false; + // Trying to change the focus if this isn't the topmost overlay can + // instead cause the FocusManager to override another overlay's focus, + // e.g. if an overlay above this one is in the process of being reloaded. + // But the C++ handler calls this method directly on ManageProfileOverlay, + // so check the pageDiv to also include its subclasses (in particular + // CreateProfileOverlay, which has higher sub-overlays). + if (OptionsPage.getTopmostVisiblePage().pageDiv == this.pageDiv) { + // This will only have an effect if the 'create-profile-name' element + // is visible, i.e. if the overlay is in create mode. + $('create-profile-name').focus(); + } + $('create-profile-ok').disabled = false; + }, + + /** + * Set a dictionary of all profile names. These are used to prevent the + * user from naming two profiles the same. + * @param {Object} profileNames A dictionary of profile names. + * @private + */ + receiveProfileNames_: function(profileNames) { + this.profileNames_ = profileNames; + }, + + /** + * Callback to show the add/remove shortcut buttons when in edit mode, + * called by the handler as a result of the 'requestHasProfileShortcuts_' + * message. + * @param {boolean} hasShortcuts Whether profile has any existing shortcuts. + * @private + */ + receiveHasProfileShortcuts_: function(hasShortcuts) { + $('add-shortcut-button').hidden = hasShortcuts; + $('remove-shortcut-button').hidden = !hasShortcuts; + }, + + /** + * Display the error bubble, with |errorText| in the bubble. + * @param {string} errorText The string to display as an error. + * @param {string} mode A label that specifies the type of dialog + * box which is currently being viewed (i.e. 'create' or + * 'manage'). + * @param {boolean} disableOKButton True if the dialog's OK button should be + * disabled when the error bubble is shown. It will be (re-)enabled when + * the error bubble is hidden. + * @private + */ + showErrorBubble_: function(errorText, mode, disableOKButton) { + var nameErrorEl = $(mode + '-profile-error-bubble'); + nameErrorEl.hidden = false; + nameErrorEl.textContent = errorText; + + if (disableOKButton) + $(mode + '-profile-ok').disabled = true; + }, + + /** + * Hide the error bubble. + * @param {string} mode A label that specifies the type of dialog + * box which is currently being viewed (i.e. 'create' or + * 'manage'). + * @private + */ + hideErrorBubble_: function(mode) { + $(mode + '-profile-error-bubble').hidden = true; + $(mode + '-profile-ok').disabled = false; + }, + + /** + * oninput callback for <input> field. + * @param {Event} event The event object. + * @param {string} mode A label that specifies the type of dialog + * box which is currently being viewed (i.e. 'create' or + * 'manage'). + * @private + */ + onNameChanged_: function(event, mode) { + var newName = event.target.value; + var oldName = this.profileInfo_.name; + + if (newName == oldName) { + this.hideErrorBubble_(mode); + } else if (this.profileNames_[newName] != undefined) { + var errorText = + loadTimeData.getString('manageProfilesDuplicateNameError'); + this.showErrorBubble_(errorText, mode, true); + } else { + this.hideErrorBubble_(mode); + + var nameIsValid = $(mode + '-profile-name').validity.valid; + $(mode + '-profile-ok').disabled = !nameIsValid; + } + }, + + /** + * Called when the user clicks "OK" or hits enter. Saves the newly changed + * profile info. + * @private + */ + submitManageChanges_: function() { + var name = $('manage-profile-name').value; + var iconURL = $('manage-profile-icon-grid').selectedItem; + + chrome.send('setProfileIconAndName', + [this.profileInfo_.filePath, iconURL, name]); + }, + + /** + * Called when the user clicks "OK" or hits enter. Creates the profile + * using the information in the dialog. + * @private + */ + submitCreateProfile_: function() { + // This is visual polish: the UI to access this should be disabled for + // managed users, and the back end will prevent user creation anyway. + if (this.profileInfo_ && this.profileInfo_.isManaged) + return; + + this.hideErrorBubble_('create'); + CreateProfileOverlay.updateCreateInProgress(true); + + // Get the user's chosen name and icon, or default if they do not + // wish to customize their profile. + var name = $('create-profile-name').value; + var iconUrl = $('create-profile-icon-grid').selectedItem; + var createShortcut = $('create-shortcut').checked; + var isManaged = $('create-profile-managed').checked; + var existingManagedUserId = ''; + + // 'createProfile' is handled by the CreateProfileHandler. + chrome.send('createProfile', + [name, iconUrl, createShortcut, + isManaged, existingManagedUserId]); + }, + + /** + * Called when the selected icon in the icon grid changes. + * @param {string} mode A label that specifies the type of dialog + * box which is currently being viewed (i.e. 'create' or + * 'manage'). + * @private + */ + onIconGridSelectionChanged_: function(mode) { + var iconURL = $(mode + '-profile-icon-grid').selectedItem; + if (!iconURL || iconURL == this.iconGridSelectedURL_) + return; + this.iconGridSelectedURL_ = iconURL; + if (this.profileInfo_ && this.profileInfo_.filePath) { + chrome.send('profileIconSelectionChanged', + [this.profileInfo_.filePath, iconURL]); + } + }, + + /** + * Updates the contents of the "Manage Profile" section of the dialog, + * and shows that section. + * @private + */ + prepareForManageDialog_: function() { + var profileInfo = BrowserOptions.getCurrentProfile(); + ManageProfileOverlay.setProfileInfo(profileInfo, 'manage'); + $('manage-profile-overlay-create').hidden = true; + $('manage-profile-overlay-manage').hidden = false; + $('manage-profile-overlay-delete').hidden = true; + $('manage-profile-name').disabled = profileInfo.isManaged; + this.hideErrorBubble_('manage'); + }, + + /** + * Display the "Manage Profile" dialog. + * @private + */ + showManageDialog_: function() { + this.prepareForManageDialog_(); + OptionsPage.navigateToPage('manageProfile'); + }, + + /** + * Display the "Delete Profile" dialog. + * @param {Object} profileInfo The profile object of the profile to delete. + * @private + */ + showDeleteDialog_: function(profileInfo) { + if (BrowserOptions.getCurrentProfile().isManaged) + return; + + ManageProfileOverlay.setProfileInfo(profileInfo, 'manage'); + $('manage-profile-overlay-create').hidden = true; + $('manage-profile-overlay-manage').hidden = true; + $('manage-profile-overlay-delete').hidden = false; + $('delete-profile-icon').style.content = + imageset(profileInfo.iconURL + '@scalefactorx'); + $('delete-profile-text').textContent = + loadTimeData.getStringF('deleteProfileMessage', profileInfo.name); + $('delete-managed-profile-addendum').hidden = !profileInfo.isManaged; + + // Because this dialog isn't useful when refreshing or as part of the + // history, don't create a history entry for it when showing. + OptionsPage.showPageByName('manageProfile', false); + }, + + /** + * Display the "Create Profile" dialog. + * @private + */ + showCreateDialog_: function() { + OptionsPage.navigateToPage('createProfile'); + }, + }; + + // Forward public APIs to private implementations. + [ + 'receiveDefaultProfileIcons', + 'receiveNewProfileDefaults', + 'receiveProfileNames', + 'receiveHasProfileShortcuts', + 'setProfileInfo', + 'setProfileName', + 'showManageDialog', + 'showDeleteDialog', + 'showCreateDialog', + ].forEach(function(name) { + ManageProfileOverlay[name] = function() { + var instance = ManageProfileOverlay.getInstance(); + return instance[name + '_'].apply(instance, arguments); + }; + }); + + function CreateProfileOverlay() { + OptionsPage.call(this, 'createProfile', + loadTimeData.getString('createProfileTabTitle'), + 'manage-profile-overlay'); + }; + + cr.addSingletonGetter(CreateProfileOverlay); + + CreateProfileOverlay.prototype = { + // Inherit from ManageProfileOverlay. + __proto__: ManageProfileOverlay.prototype, + + // The signed-in email address of the current profile, or empty if they're + // not signed in. + signedInEmail_: '', + + /** @override */ + canShowPage: function() { + return !BrowserOptions.getCurrentProfile().isManaged; + }, + + /** + * Configures the overlay to the "create user" mode. + * @override + */ + didShowPage: function() { + chrome.send('requestCreateProfileUpdate'); + chrome.send('requestDefaultProfileIcons'); + chrome.send('requestNewProfileDefaults'); + + $('manage-profile-overlay-create').hidden = false; + $('manage-profile-overlay-manage').hidden = true; + $('manage-profile-overlay-delete').hidden = true; + $('create-profile-instructions').textContent = + loadTimeData.getStringF('createProfileInstructions'); + this.hideErrorBubble_(); + this.updateCreateInProgress_(false); + + var shortcutsEnabled = loadTimeData.getBoolean('profileShortcutsEnabled'); + $('create-shortcut-container').hidden = !shortcutsEnabled; + $('create-shortcut').checked = shortcutsEnabled; + + $('create-profile-name-label').hidden = true; + $('create-profile-name').hidden = true; + $('create-profile-ok').disabled = true; + + $('create-profile-managed').checked = false; + $('create-profile-managed-signed-in').disabled = true; + $('create-profile-managed-signed-in').hidden = true; + $('create-profile-managed-not-signed-in').hidden = true; + }, + + /** @override */ + handleCancel: function() { + this.cancelCreateProfile_(); + }, + + /** @override */ + showErrorBubble_: function(errorText) { + ManageProfileOverlay.getInstance().showErrorBubble_(errorText, + 'create', + false); + }, + + /** @override */ + hideErrorBubble_: function() { + ManageProfileOverlay.getInstance().hideErrorBubble_('create'); + }, + + /** + * Updates the UI when a profile create step begins or ends. + * Note that hideErrorBubble_() also enables the "OK" button, so it + * must be called before this function if both are used. + * @param {boolean} inProgress True if the UI should be updated to show that + * profile creation is now in progress. + * @private + */ + updateCreateInProgress_: function(inProgress) { + this.createInProgress_ = inProgress; + this.updateCreateManagedUserCheckbox_(); + + $('create-profile-icon-grid').disabled = inProgress; + $('create-profile-name').disabled = inProgress; + $('create-shortcut').disabled = inProgress; + $('create-profile-ok').disabled = inProgress; + + $('create-profile-throbber').hidden = !inProgress; + }, + + /** + * Cancels the creation of the a profile. It is safe to call this even + * when no profile is in the process of being created. + * @private + */ + cancelCreateProfile_: function() { + OptionsPage.closeOverlay(); + chrome.send('cancelCreateProfile'); + this.hideErrorBubble_(); + this.updateCreateInProgress_(false); + }, + + /** + * Shows an error message describing an error that occurred while creating + * a new profile. + * Called by BrowserOptions via the BrowserOptionsHandler. + * @param {string} error The error message to display. + * @private + */ + onError_: function(error) { + this.updateCreateInProgress_(false); + this.showErrorBubble_(error); + }, + + /** + * Shows a warning message giving information while creating a new profile. + * Called by BrowserOptions via the BrowserOptionsHandler. + * @param {string} warning The warning message to display. + * @private + */ + onWarning_: function(warning) { + this.showErrorBubble_(warning); + }, + + /** + * For new supervised users, shows a confirmation page after successfully + * creating a new profile; otherwise, the handler will open a new window. + * @param {Object} profileInfo An object of the form: + * profileInfo = { + * name: "Profile Name", + * filePath: "/path/to/profile/data/on/disk" + * isManaged: (true|false), + * }; + * @private + */ + onSuccess_: function(profileInfo) { + this.updateCreateInProgress_(false); + OptionsPage.closeOverlay(); + if (profileInfo.isManaged) { + profileInfo.custodianEmail = this.signedInEmail_; + ManagedUserCreateConfirmOverlay.setProfileInfo(profileInfo); + OptionsPage.showPageByName('managedUserCreateConfirm', false); + BrowserOptions.updateManagesSupervisedUsers(true); + } + }, + + /** + * Updates the signed-in or not-signed-in UI when in create mode. Called by + * the handler in response to the 'requestCreateProfileUpdate' message. + * updateManagedUsersAllowed_ is expected to be called after this is, and + * will update additional UI elements. + * @param {string} email The email address of the currently signed-in user. + * An empty string indicates that the user is not signed in. + * @param {boolean} hasError Whether the user's sign-in credentials are + * still valid. + * @private + */ + updateSignedInStatus_: function(email, hasError) { + this.signedInEmail_ = email; + this.hasError_ = hasError; + var isSignedIn = email !== ''; + $('create-profile-managed-signed-in').hidden = !isSignedIn; + $('create-profile-managed-not-signed-in').hidden = isSignedIn; + + if (isSignedIn) { + var accountDetailsOutOfDate = + $('create-profile-managed-account-details-out-of-date-label'); + accountDetailsOutOfDate.textContent = loadTimeData.getStringF( + 'manageProfilesManagedAccountDetailsOutOfDate', email); + accountDetailsOutOfDate.hidden = !hasError; + + $('create-profile-managed-signed-in-label').textContent = + loadTimeData.getStringF( + 'manageProfilesManagedSignedInLabel', email); + $('create-profile-managed-signed-in-label').hidden = hasError; + + $('create-profile-managed-sign-in-again-link').hidden = !hasError; + $('create-profile-managed-signed-in-learn-more-link').hidden = hasError; + } + + this.updateImportExistingManagedUserLink_(isSignedIn && !hasError); + }, + + /** + * Enables/disables the 'import existing managed users' link button. + * It also updates the button text. + * @param {boolean} enable True to enable the link button and + * false otherwise. + * @private + */ + updateImportExistingManagedUserLink_: function(enable) { + var importManagedUserElement = $('import-existing-managed-user-link'); + importManagedUserElement.disabled = !enable; + importManagedUserElement.textContent = enable ? + loadTimeData.getString('importExistingManagedUserLink') : + loadTimeData.getString('signInToImportManagedUsers'); + }, + + /** + * Sets whether creating managed users is allowed or not. Called by the + * handler in response to the 'requestCreateProfileUpdate' message or a + * change in the (policy-controlled) pref that prohibits creating managed + * users, after the signed-in status has been updated. + * @param {boolean} allowed True if creating managed users should be + * allowed. + * @private + */ + updateManagedUsersAllowed_: function(allowed) { + this.managedUsersAllowed_ = allowed; + this.updateCreateManagedUserCheckbox_(); + + $('create-profile-managed-not-signed-in-link').hidden = !allowed; + if (!allowed) { + $('create-profile-managed-indicator').setAttribute('controlled-by', + 'policy'); + } else { + $('create-profile-managed-indicator').removeAttribute('controlled-by'); + } + }, + + /** + * Updates the status of the "create managed user" checkbox. Called from + * updateManagedUsersAllowed_() or updateCreateInProgress_(). + * updateSignedInStatus_() does not call this method directly, because it + * will be followed by a call to updateManagedUsersAllowed_(). + * @private + */ + updateCreateManagedUserCheckbox_: function() { + $('create-profile-managed').disabled = + !this.managedUsersAllowed_ || this.createInProgress_ || + this.signedInEmail_ == '' || this.hasError_; + }, + }; + + // Forward public APIs to private implementations. + [ + 'cancelCreateProfile', + 'onError', + 'onSuccess', + 'onWarning', + 'updateCreateInProgress', + 'updateManagedUsersAllowed', + 'updateSignedInStatus', + ].forEach(function(name) { + CreateProfileOverlay[name] = function() { + var instance = CreateProfileOverlay.getInstance(); + return instance[name + '_'].apply(instance, arguments); + }; + }); + + // Export + return { + ManageProfileOverlay: ManageProfileOverlay, + CreateProfileOverlay: CreateProfileOverlay, + }; +}); diff --git a/chromium/chrome/browser/resources/options/managed_user_create_confirm.css b/chromium/chrome/browser/resources/options/managed_user_create_confirm.css new file mode 100644 index 00000000000..a1e8af8b40b --- /dev/null +++ b/chromium/chrome/browser/resources/options/managed_user_create_confirm.css @@ -0,0 +1,46 @@ +/* Copyright 2013 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ + +#managed-user-created { + width: 722px; +} + +#managed-user-created-title { + padding: 20px; + word-wrap: break-word; +} + +@media only screen and (max-height:400px) { + +/* Omit the image on very small screens. */ +#managed-user-created-image { + display: none; +} + +} /* @media only screen and (max-height:400px) */ + +#managed-user-created-image { + -webkit-border-radius: 3px 3px 0 0; + -webkit-box-flex: 5; + background-image: -webkit-image-set( + url('../../../../ui/resources/default_100_percent/supervised_illustration_done.png') 1x, + url('../../../../ui/resources/default_200_percent/supervised_illustration_done.png') 2x); + background-position: center; + height: 344px; +} + +#managed-user-created-text { + padding: 0 20px 0 20px; + white-space: pre-wrap; + word-wrap: break-word; +} + +#managed-user-created-switch { + max-width: 600px; + word-wrap: break-word; +} + +#managed-user-created-action-area { + padding: 20px; +} diff --git a/chromium/chrome/browser/resources/options/managed_user_create_confirm.html b/chromium/chrome/browser/resources/options/managed_user_create_confirm.html new file mode 100644 index 00000000000..346b9f015a6 --- /dev/null +++ b/chromium/chrome/browser/resources/options/managed_user_create_confirm.html @@ -0,0 +1,13 @@ +<div id="managed-user-created" class="page" hidden> + <div class="close-button"></div> + <!-- Overlay for the confirmation after creating a supervised user. --> + <div id="managed-user-created-image"></div> + <h1 id="managed-user-created-title"></h1> + <div id="managed-user-created-text" class="content-area"></div> + <div id="managed-user-created-action-area" class="action-area button-strip"> + <button id="managed-user-created-done" + i18n-content="managedUserCreatedDone"> + </button> + <button id="managed-user-created-switch"></button> + </div> +</div> diff --git a/chromium/chrome/browser/resources/options/managed_user_create_confirm.js b/chromium/chrome/browser/resources/options/managed_user_create_confirm.js new file mode 100644 index 00000000000..de91bf9be9d --- /dev/null +++ b/chromium/chrome/browser/resources/options/managed_user_create_confirm.js @@ -0,0 +1,118 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + var OptionsPage = options.OptionsPage; + + /** + * ManagedUserCreateConfirm class. + * Encapsulated handling of the confirmation overlay page when creating a + * managed user. + * @constructor + * @class + */ + function ManagedUserCreateConfirmOverlay() { + OptionsPage.call(this, 'managedUserCreateConfirm', + '', // The title will be based on the new profile name. + 'managed-user-created'); + }; + + cr.addSingletonGetter(ManagedUserCreateConfirmOverlay); + + ManagedUserCreateConfirmOverlay.prototype = { + // Inherit from OptionsPage. + __proto__: OptionsPage.prototype, + + // Info about the newly created profile. + profileInfo_: null, + + /** + * Initialize the page. + */ + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + + $('managed-user-created-done').onclick = function(event) { + OptionsPage.closeOverlay(); + }; + + var self = this; + + $('managed-user-created-switch').onclick = function(event) { + OptionsPage.closeOverlay(); + chrome.send('switchToProfile', [self.profileInfo_.filePath]); + }; + }, + + /** @override */ + didShowPage: function() { + $('managed-user-created-switch').focus(); + }, + + /** + * Sets the profile info used in the dialog and updates the profile name + * displayed. Called by the profile creation overlay when this overlay is + * opened. + * @param {Object} info An object of the form: + * info = { + * name: "Profile Name", + * filePath: "/path/to/profile/data/on/disk", + * isManaged: (true|false) + * custodianEmail: "example@gmail.com" + * }; + * @private + */ + setProfileInfo_: function(info) { + function HTMLEscape(original) { + return original.replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + + var MAX_LENGTH = 50; + function elide(original) { + if (original.length <= MAX_LENGTH) + return original; + return original.substring(0, MAX_LENGTH - 3) + '...'; + } + + this.profileInfo_ = info; + var elidedName = elide(info.name); + $('managed-user-created-title').textContent = + loadTimeData.getStringF('managedUserCreatedTitle', elidedName); + $('managed-user-created-switch').textContent = + loadTimeData.getStringF('managedUserCreatedSwitch', elidedName); + + // HTML-escape the user-supplied strings before putting them into + // innerHTML. This is probably excessive for the email address, but + // belt-and-suspenders is cheap here. + $('managed-user-created-text').innerHTML = + loadTimeData.getStringF('managedUserCreatedText', + HTMLEscape(elidedName), + HTMLEscape(elide(info.custodianEmail))); + }, + + /** @override */ + canShowPage: function() { + return this.profileInfo_ != null; + }, + }; + + // Forward public APIs to private implementations. + [ + 'setProfileInfo', + ].forEach(function(name) { + ManagedUserCreateConfirmOverlay[name] = function() { + var instance = ManagedUserCreateConfirmOverlay.getInstance(); + return instance[name + '_'].apply(instance, arguments); + }; + }); + + // Export + return { + ManagedUserCreateConfirmOverlay: ManagedUserCreateConfirmOverlay, + }; +}); diff --git a/chromium/chrome/browser/resources/options/managed_user_import.css b/chromium/chrome/browser/resources/options/managed_user_import.css new file mode 100644 index 00000000000..9dae8f1db4e --- /dev/null +++ b/chromium/chrome/browser/resources/options/managed_user_import.css @@ -0,0 +1,75 @@ +/* Copyright 2013 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ + +#create-new-user-link { + position: absolute; +} + +#managed-user-import { + width: 612px; +} + +#managed-user-import-text { + padding-bottom: 10px; + padding-left: 17px; + white-space: pre-wrap; + word-wrap: break-word; +} + +#managed-user-list { + height: 240px; + margin-bottom: 10px; +} + +#managed-user-list .profile-name { + -webkit-box-flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +#managed-user-list > * { + height: 40px; +} + +#managed-user-list:focus { + border-color: rgb(77, 144, 254); +} + +#select-avatar-grid { + background-color: rgba(255, 255, 255, 0.75); + padding: 2px; +} + +#managed-user-import-error-bubble { + -webkit-transition: max-height 200ms, padding 200ms; + background-color: rgb(238, 185, 57); + border-radius: 4px; + font-weight: bold; + margin-left: auto; + margin-right: auto; + margin-top: 10px; + max-height: 50px; + overflow: hidden; + padding: 1px 10px; + text-align: center; + width: 80%; +} + +#managed-user-import-error-bubble[hidden] { + max-height: 0; +} + +.profile-img-disabled { + opacity: 0.4; +} + +.profile-name-disabled { + color: rgb(153, 153, 153); +} + +.already-on-this-device { + padding-left: 20px; + padding-right: 6px; +} diff --git a/chromium/chrome/browser/resources/options/managed_user_import.html b/chromium/chrome/browser/resources/options/managed_user_import.html new file mode 100644 index 00000000000..db2674dc953 --- /dev/null +++ b/chromium/chrome/browser/resources/options/managed_user_import.html @@ -0,0 +1,24 @@ +<div id="managed-user-import" class="page" hidden> + <div class="close-button"></div> + <!-- Overlay to import an existing managed user during user creation --> + <h1 id="managed-user-import-title"></h1> + <div id="managed-user-import-text"> + </div> + <div id="managed-user-import-content-area" class="content-area"> + <list id="managed-user-list" class="settings-list"></list> + <grid id="select-avatar-grid" hidden></grid> + </div> + <div id="managed-user-import-error-bubble" hidden></div> + <div id="managed-user-import-action-area" class="action-area"> + <button id="create-new-user-link" class="link-button" + i18n-content="createNewUserLink"> + </button> + <div class="button-strip"> + <div id="managed-user-import-throbber" class="throbber"></div> + <button id="managed-user-import-cancel" i18n-content="cancel"> + </button> + <button id="managed-user-import-ok" class="default-button"> + </button> + </div> + </div> +</div>
\ No newline at end of file diff --git a/chromium/chrome/browser/resources/options/managed_user_import.js b/chromium/chrome/browser/resources/options/managed_user_import.js new file mode 100644 index 00000000000..70a54dd2cab --- /dev/null +++ b/chromium/chrome/browser/resources/options/managed_user_import.js @@ -0,0 +1,238 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + var OptionsPage = options.OptionsPage; + var ArrayDataModel = cr.ui.ArrayDataModel; + + /** + * ManagedUserImportOverlay class. + * Encapsulated handling of the 'Import existing managed user' overlay page. + * @constructor + * @class + */ + function ManagedUserImportOverlay() { + var title = loadTimeData.getString('managedUserImportTitle'); + OptionsPage.call(this, 'managedUserImport', + title, 'managed-user-import'); + }; + + cr.addSingletonGetter(ManagedUserImportOverlay); + + ManagedUserImportOverlay.prototype = { + // Inherit from OptionsPage. + __proto__: OptionsPage.prototype, + + /** @override */ + canShowPage: function() { + return !BrowserOptions.getCurrentProfile().isManaged; + }, + + /** + * Initialize the page. + */ + initializePage: function() { + // Call base class implementation to start preference initialization. + OptionsPage.prototype.initializePage.call(this); + + var managedUserList = $('managed-user-list'); + options.managedUserOptions.ManagedUserList.decorate(managedUserList); + + var avatarGrid = $('select-avatar-grid'); + options.ProfilesIconGrid.decorate(avatarGrid); + var avatarIcons = loadTimeData.getValue('avatarIcons'); + avatarGrid.dataModel = new ArrayDataModel(avatarIcons); + + managedUserList.addEventListener('change', function(event) { + var managedUser = managedUserList.selectedItem; + if (!managedUser) + return; + + $('managed-user-import-ok').disabled = + managedUserList.selectedItem.onCurrentDevice; + }); + + var self = this; + $('managed-user-import-cancel').onclick = function(event) { + OptionsPage.closeOverlay(); + self.updateImportInProgress_(false); + + // 'cancelCreateProfile' is handled by CreateProfileHandler. + chrome.send('cancelCreateProfile'); + }; + + $('managed-user-import-ok').onclick = + this.showAvatarGridOrSubmit_.bind(this); + + $('create-new-user-link').onclick = function(event) { + OptionsPage.closeOverlay(); + OptionsPage.navigateToPage('createProfile'); + }; + }, + + /** + * @override + */ + didShowPage: function() { + chrome.send('requestManagedUserImportUpdate'); + + this.updateImportInProgress_(false); + $('managed-user-import-error-bubble').hidden = true; + $('managed-user-import-ok').disabled = true; + $('select-avatar-grid').hidden = true; + $('managed-user-list').hidden = false; + + $('managed-user-import-ok').textContent = + loadTimeData.getString('managedUserImportOk'); + $('managed-user-import-text').textContent = + loadTimeData.getString('managedUserImportText'); + $('managed-user-import-title').textContent = + loadTimeData.getString('managedUserImportTitle'); + }, + + /** + * Called when the user clicks the "OK" button. In case the managed + * user being imported has no avatar in sync, it shows the avatar + * icon grid. In case the avatar grid is visible or the managed user + * already has an avatar stored in sync, it proceeds with importing + * the managed user. + * @private + */ + showAvatarGridOrSubmit_: function() { + var managedUser = $('managed-user-list').selectedItem; + if (!managedUser) + return; + + $('managed-user-import-error-bubble').hidden = true; + + if ($('select-avatar-grid').hidden && managedUser.needAvatar) { + this.showAvatarGridHelper_(); + return; + } + + var avatarUrl = managedUser.needAvatar ? + $('select-avatar-grid').selectedItem : managedUser.iconURL; + + this.updateImportInProgress_(true); + + // 'createProfile' is handled by CreateProfileHandler. + chrome.send('createProfile', [managedUser.name, avatarUrl, + false, true, managedUser.id]); + }, + + /** + * Hides the 'managed user list' and shows the avatar grid instead. + * It also updates the overlay text and title to instruct the user + * to choose an avatar for the supervised user. + * @private + */ + showAvatarGridHelper_: function() { + $('managed-user-list').hidden = true; + $('select-avatar-grid').hidden = false; + $('select-avatar-grid').redraw(); + $('select-avatar-grid').selectedItem = + loadTimeData.getValue('avatarIcons')[0]; + + $('managed-user-import-ok').textContent = + loadTimeData.getString('managedUserSelectAvatarOk'); + $('managed-user-import-text').textContent = + loadTimeData.getString('managedUserSelectAvatarText'); + $('managed-user-import-title').textContent = + loadTimeData.getString('managedUserSelectAvatarTitle'); + }, + + /** + * Updates the UI according to the importing state. + * @param {boolean} inProgress True to indicate that + * importing is in progress and false otherwise. + * @private + */ + updateImportInProgress_: function(inProgress) { + $('managed-user-import-ok').disabled = inProgress; + $('managed-user-list').disabled = inProgress; + $('select-avatar-grid').disabled = inProgress; + $('create-new-user-link').disabled = inProgress; + $('managed-user-import-throbber').hidden = !inProgress; + }, + + /** + * Adds all the existing |managedUsers| to the list. If |managedUsers| + * is undefined, then the list is cleared. + * @param {Array.<Object>} managedUsers An array of managed user objects. + * Each object is of the form: + * managedUser = { + * id: "Managed User ID", + * name: "Managed User Name", + * iconURL: "chrome://path/to/icon/image", + * onCurrentDevice: true or false, + * needAvatar: true or false + * } + * @private + */ + receiveExistingManagedUsers_: function(managedUsers) { + if (!managedUsers) { + $('managed-user-list').dataModel = null; + return; + } + + managedUsers.sort(function(a, b) { + return a.name.localeCompare(b.name); + }); + + $('managed-user-list').dataModel = new ArrayDataModel(managedUsers); + if (managedUsers.length == 0) { + this.onError_(loadTimeData.getString('noExistingManagedUsers')); + $('managed-user-import-ok').disabled = true; + } + }, + + /** + * @private + */ + hideErrorBubble_: function() { + $('managed-user-import-error-bubble').hidden = true; + }, + + /** + * Displays an error message if an error occurs while + * importing a managed user. + * Called by BrowserOptions via the BrowserOptionsHandler. + * @param {string} error The error message to display. + * @private + */ + onError_: function(error) { + var errorBubble = $('managed-user-import-error-bubble'); + errorBubble.hidden = false; + errorBubble.textContent = error; + this.updateImportInProgress_(false); + }, + + /** + * Closes the overlay if importing the managed user was successful. + * @private + */ + onSuccess_: function() { + this.updateImportInProgress_(false); + OptionsPage.closeOverlay(); + }, + }; + + // Forward public APIs to private implementations. + [ + 'hideErrorBubble', + 'onError', + 'onSuccess', + 'receiveExistingManagedUsers', + ].forEach(function(name) { + ManagedUserImportOverlay[name] = function() { + var instance = ManagedUserImportOverlay.getInstance(); + return instance[name + '_'].apply(instance, arguments); + }; + }); + + // Export + return { + ManagedUserImportOverlay: ManagedUserImportOverlay, + }; +}); diff --git a/chromium/chrome/browser/resources/options/managed_user_learn_more.css b/chromium/chrome/browser/resources/options/managed_user_learn_more.css new file mode 100644 index 00000000000..d22a9a7b841 --- /dev/null +++ b/chromium/chrome/browser/resources/options/managed_user_learn_more.css @@ -0,0 +1,40 @@ +/* Copyright 2013 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ + +#managed-user-learn-more { + width: 722px; +} + +#managed-user-learn-more-title { + padding: 20px; +} + +#managed-user-learn-more-text { + padding: 0 20px 0 20px; + white-space: pre-wrap; + word-wrap: break-word; +} + +@media only screen and (max-height:500px) { + +/* Omit the image on very small screens. */ +#managed-user-learn-more-image { + display: none; +} + +} /* @media only screen and (max-height:500px) */ + +#managed-user-learn-more-image { + -webkit-border-radius: 3px 3px 0 0; + -webkit-box-flex: 1; + background-image: -webkit-image-set( + url('../../../../ui/resources/default_100_percent/supervised_illustration_start.png') 1x, + url('../../../../ui/resources/default_200_percent/supervised_illustration_start.png') 2x); + background-position: center; + height: 344px; +} + +#managed-user-learn-more-action-area { + padding: 20px; +} diff --git a/chromium/chrome/browser/resources/options/managed_user_learn_more.html b/chromium/chrome/browser/resources/options/managed_user_learn_more.html new file mode 100644 index 00000000000..5eea88128e6 --- /dev/null +++ b/chromium/chrome/browser/resources/options/managed_user_learn_more.html @@ -0,0 +1,17 @@ +<div id="managed-user-learn-more" class="page" hidden> + <div class="close-button"></div> + <!-- Overlay for the 'Learn more' link when creating a supervised user. --> + <div id="managed-user-learn-more-image"></div> + <h1 id="managed-user-learn-more-title" + i18n-content="managedUserLearnMoreTitle"> + </h1> + <div id="managed-user-learn-more-text" class="content-area" + i18n-values=".innerHTML:managedUserLearnMoreText"> + </div> + <div id="managed-user-learn-more-action-area" + class="action-area button-strip"> + <button id="managed-user-learn-more-done" + i18n-content="managedUserLearnMoreDone"> + </button> + </div> +</div> diff --git a/chromium/chrome/browser/resources/options/managed_user_learn_more.js b/chromium/chrome/browser/resources/options/managed_user_learn_more.js new file mode 100644 index 00000000000..ab9d76ddd78 --- /dev/null +++ b/chromium/chrome/browser/resources/options/managed_user_learn_more.js @@ -0,0 +1,42 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + var OptionsPage = options.OptionsPage; + + /** + * ManagedUserLearnMore class. + * Encapsulated handling of the 'Learn more...' overlay page. + * @constructor + * @class + */ + function ManagedUserLearnMoreOverlay() { + OptionsPage.call(this, 'managedUserLearnMore', + loadTimeData.getString('managedUserLearnMoreTitle'), + 'managed-user-learn-more'); + }; + + cr.addSingletonGetter(ManagedUserLearnMoreOverlay); + + ManagedUserLearnMoreOverlay.prototype = { + // Inherit from OptionsPage. + __proto__: OptionsPage.prototype, + + /** + * Initialize the page. + */ + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + + $('managed-user-learn-more-done').onclick = function(event) { + OptionsPage.closeOverlay(); + }; + }, + }; + + // Export + return { + ManagedUserLearnMoreOverlay: ManagedUserLearnMoreOverlay, + }; +}); diff --git a/chromium/chrome/browser/resources/options/managed_user_list.js b/chromium/chrome/browser/resources/options/managed_user_list.js new file mode 100644 index 00000000000..4af84c64c14 --- /dev/null +++ b/chromium/chrome/browser/resources/options/managed_user_list.js @@ -0,0 +1,117 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options.managedUserOptions', function() { + /** @const */ var List = cr.ui.List; + /** @const */ var ListItem = cr.ui.ListItem; + /** @const */ var ListSingleSelectionModel = cr.ui.ListSingleSelectionModel; + + /** + * Create a new managed user list item. + * @param {Object} entry The managed user this item represents. + * It has the following form: + * managedUser = { + * id: "Managed User ID", + * name: "Managed User Name", + * iconURL: "chrome://path/to/icon/image", + * onCurrentDevice: true or false, + * needAvatar: true or false + * } + * @constructor + * @extends {cr.ui.ListItem} + */ + function ManagedUserListItem(entry) { + var el = cr.doc.createElement('div'); + el.managedUser_ = entry; + el.__proto__ = ManagedUserListItem.prototype; + el.decorate(); + return el; + } + + ManagedUserListItem.prototype = { + __proto__: ListItem.prototype, + + /** + * @type {string} the ID of this managed user list item. + */ + get id() { + return this.managedUser_.id; + }, + + /** + * @type {string} the name of this managed user list item. + */ + get name() { + return this.managedUser_.name; + }, + + /** + * @type {string} the path to the avatar icon of this managed + * user list item. + */ + get iconURL() { + return this.managedUser_.iconURL; + }, + + /** @override */ + decorate: function() { + ListItem.prototype.decorate.call(this); + var managedUser = this.managedUser_; + + // Add the avatar. + var iconElement = this.ownerDocument.createElement('img'); + iconElement.className = 'profile-img'; + iconElement.style.content = + imageset(managedUser.iconURL + '@scalefactorx'); + this.appendChild(iconElement); + + // Add the profile name. + var nameElement = this.ownerDocument.createElement('div'); + nameElement.className = 'profile-name'; + nameElement.textContent = managedUser.name; + this.appendChild(nameElement); + + if (managedUser.onCurrentDevice) { + iconElement.className += ' profile-img-disabled'; + nameElement.className += ' profile-name-disabled'; + + // Add "(already on this device)" message. + var alreadyOnDeviceElement = this.ownerDocument.createElement('div'); + alreadyOnDeviceElement.className = + 'profile-name-disabled already-on-this-device'; + alreadyOnDeviceElement.textContent = + loadTimeData.getString('managedUserAlreadyOnThisDevice'); + this.appendChild(alreadyOnDeviceElement); + } + }, + }; + + /** + * Create a new managed users list. + * @constructor + * @extends {cr.ui.List} + */ + var ManagedUserList = cr.ui.define('list'); + + ManagedUserList.prototype = { + __proto__: List.prototype, + + /** @override */ + createItem: function(entry) { + return new ManagedUserListItem(entry); + }, + + /** @override */ + decorate: function() { + List.prototype.decorate.call(this); + this.selectionModel = new ListSingleSelectionModel(); + this.autoExpands = true; + }, + }; + + return { + ManagedUserListItem: ManagedUserListItem, + ManagedUserList: ManagedUserList, + }; +}); diff --git a/chromium/chrome/browser/resources/options/media_galleries_list.js b/chromium/chrome/browser/resources/options/media_galleries_list.js new file mode 100644 index 00000000000..8d892bcdd00 --- /dev/null +++ b/chromium/chrome/browser/resources/options/media_galleries_list.js @@ -0,0 +1,59 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + /** @const */ var DeletableItem = options.DeletableItem; + /** @const */ var DeletableItemList = options.DeletableItemList; + + /** + * @constructor + * @extends {DeletableItem} + */ + function MediaGalleriesListItem(galleryInfo) { + var el = cr.doc.createElement('div'); + el.galleryInfo_ = galleryInfo; + el.__proto__ = MediaGalleriesListItem.prototype; + el.decorate(); + return el; + } + + MediaGalleriesListItem.prototype = { + __proto__: DeletableItem.prototype, + + decorate: function() { + DeletableItem.prototype.decorate.call(this); + + var span = this.ownerDocument.createElement('span'); + span.textContent = this.galleryInfo_.displayName; + this.contentElement.appendChild(span); + this.contentElement.title = this.galleryInfo_.path; + }, + }; + + var MediaGalleriesList = cr.ui.define('list'); + + MediaGalleriesList.prototype = { + __proto__: DeletableItemList.prototype, + + /** @override */ + decorate: function() { + DeletableItemList.prototype.decorate.call(this); + this.autoExpands_ = true; + }, + + /** @override */ + createItem: function(galleryInfo) { + return new MediaGalleriesListItem(galleryInfo); + }, + + /** @override */ + deleteItemAtIndex: function(index) { + chrome.send('forgetGallery', [this.dataModel.item(index).id]); + }, + }; + + return { + MediaGalleriesList: MediaGalleriesList + }; +}); diff --git a/chromium/chrome/browser/resources/options/media_galleries_manager_overlay.html b/chromium/chrome/browser/resources/options/media_galleries_manager_overlay.html new file mode 100644 index 00000000000..05c4db1c6af --- /dev/null +++ b/chromium/chrome/browser/resources/options/media_galleries_manager_overlay.html @@ -0,0 +1,19 @@ +<div id="manage-media-galleries-overlay" class="page" hidden> + <div class="close-button"></div> + <h1 i18n-content="manageMediaGalleries"></h1> + <div class="content-area"> + <list id="available-galleries-list"></list> + </div> + <div class="action-area"> + <div class="stretch"> + <button id="new-media-gallery" i18n-content="addNewGalleryButton" + disabled></button> + </div> + </stretch> + <div class="button-strip"> + <button id="manage-media-confirm" class="default-button" + i18n-content="done"> + </button> + </div> + </div> +</div> diff --git a/chromium/chrome/browser/resources/options/media_galleries_manager_overlay.js b/chromium/chrome/browser/resources/options/media_galleries_manager_overlay.js new file mode 100644 index 00000000000..ac8e5271580 --- /dev/null +++ b/chromium/chrome/browser/resources/options/media_galleries_manager_overlay.js @@ -0,0 +1,81 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + /** @const */ var ArrayDataModel = cr.ui.ArrayDataModel; + /** @const */ var OptionsPage = options.OptionsPage; + + /** + * This class is an overlay which allows the user to add or remove media + * galleries, and displays known media galleries. + * @constructor + * @extends {OptionsPage} + */ + function MediaGalleriesManager() { + OptionsPage.call(this, 'manageGalleries', + loadTimeData.getString('manageMediaGalleriesTabTitle'), + 'manage-media-galleries-overlay'); + } + + cr.addSingletonGetter(MediaGalleriesManager); + + MediaGalleriesManager.prototype = { + __proto__: OptionsPage.prototype, + + /** + * Decorate the overlay and set up event handlers. + */ + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + + this.availableGalleriesList_ = $('available-galleries-list'); + options.MediaGalleriesList.decorate(this.availableGalleriesList_); + + $('new-media-gallery').addEventListener('click', function() { + chrome.send('addNewGallery'); + }); + + $('manage-media-confirm').addEventListener( + 'click', OptionsPage.closeOverlay.bind(OptionsPage)); + + this.addEventListener('visibleChange', this.handleVisibleChange_); + }, + + /** @private */ + handleVisibleChange_: function() { + if (!this.visible) + return; + + chrome.send('initializeMediaGalleries'); + + if (this.availableGalleriesList_) + this.availableGalleriesList_.redraw(); + }, + + /** + * @param {Array} galleries List of structs describibing galleries. + * @private + */ + setAvailableMediaGalleries_: function(galleries) { + $('available-galleries-list').dataModel = new ArrayDataModel(galleries); + $('new-media-gallery').disabled = false; + $('media-galleries-section').hidden = false; + }, + }, + + // Forward public APIs to private implementations. + [ + 'setAvailableMediaGalleries', + ].forEach(function(name) { + MediaGalleriesManager[name] = function() { + var instance = MediaGalleriesManager.getInstance(); + return instance[name + '_'].apply(instance, arguments); + }; + }); + + // Export + return { + MediaGalleriesManager: MediaGalleriesManager + }; +}); diff --git a/chromium/chrome/browser/resources/options/options.html b/chromium/chrome/browser/resources/options/options.html new file mode 100644 index 00000000000..0d1f0cbb896 --- /dev/null +++ b/chromium/chrome/browser/resources/options/options.html @@ -0,0 +1,190 @@ +<!DOCTYPE HTML> +<html id="t" i18n-values="dir:textdirection"> +<head> +<meta charset="utf-8"> +<title i18n-content="optionsPageTitle"></title> +<link rel="stylesheet" href="chrome://resources/css/bubble.css"> +<link rel="stylesheet" href="chrome://resources/css/bubble_button.css"> +<link rel="stylesheet" href="chrome://resources/css/chrome_shared.css"> +<link rel="stylesheet" href="chrome://resources/css/list.css"> +<link rel="stylesheet" href="chrome://resources/css/overlay.css"> +<link rel="stylesheet" href="chrome://resources/css/spinner.css"> +<link rel="stylesheet" href="chrome://resources/css/throbber.css"> +<link rel="stylesheet" href="chrome://resources/css/tree.css"> +<link rel="stylesheet" href="../uber/uber_shared.css"> +<link rel="stylesheet" href="options_page.css"> +<link rel="stylesheet" href="alert_overlay.css"> +<link rel="stylesheet" href="autofill_edit_overlay.css"> +<link rel="stylesheet" href="autofill_options.css"> +<link rel="stylesheet" href="browser_options.css"> +<if expr="pp_ifdef('chromeos')"> + <link rel="stylesheet" href="chromeos/browser_options.css"> +</if> +<link rel="stylesheet" href="clear_browser_data_overlay.css"> +<link rel="stylesheet" href="content_settings.css"> +<link rel="stylesheet" href="controlled_setting.css"> +<link rel="stylesheet" href="cookies_view.css"> +<link rel="stylesheet" href="do_not_track_confirm_overlay.css"> +<link rel="stylesheet" href="font_settings.css"> +<link rel="stylesheet" href="handler_options.css"> +<link rel="stylesheet" href="home_page_overlay.css"> +<link rel="stylesheet" href="import_data_overlay.css"> +<if expr="not is_macosx"> +<link rel="stylesheet" href="language_dictionary_overlay.css"> +</if> +<link rel="stylesheet" href="language_options.css"> +<link rel="stylesheet" href="manage_profile_overlay.css"> +<link rel="stylesheet" href="managed_user_create_confirm.css"> +<link rel="stylesheet" href="managed_user_import.css"> +<link rel="stylesheet" href="managed_user_learn_more.css"> +<link rel="stylesheet" href="password_manager.css"> +<link rel="stylesheet" href="password_manager_list.css"> +<link rel="stylesheet" href="reset_profile_settings_banner.css"> +<link rel="stylesheet" href="reset_profile_settings_overlay.css"> +<link rel="stylesheet" href="search_engine_manager.css"> +<link rel="stylesheet" href="search_page.css"> +<link rel="stylesheet" href="spelling_confirm_overlay.css"> +<link rel="stylesheet" href="subpages_tab_controls.css"> +<link rel="stylesheet" href="startup_overlay.css"> +<link rel="stylesheet" href="../sync_setup_overlay.css"> +<if expr="pp_ifdef('chromeos')"> +<link rel="stylesheet" href="chromeos/accounts_options_page.css"> +<link rel="stylesheet" href="chromeos/bluetooth.css"> +<link rel="stylesheet" href="chromeos/change_picture_options.css"> +<link rel="stylesheet" href="chromeos/display_options.css"> +<link rel="stylesheet" href="chromeos/display_overscan.css"> +<link rel="stylesheet" href="chromeos/internet_detail.css"> +<link rel="stylesheet" href="chromeos/keyboard_overlay.css"> +<link rel="stylesheet" href="chromeos/pointer_overlay.css"> +<link rel="stylesheet" href="factory_reset_overlay.css"> +</if> +<if expr="pp_ifdef('use_nss')"> +<link rel="stylesheet" href="certificate_manager.css"> +<link rel="stylesheet" href="certificate_tree.css"> +</if> +<if expr="pp_ifdef('enable_settings_app')"> +<link rel="stylesheet" href="options_settings_app.css"> +</if> +<script src="chrome://resources/css/tree.css.js"></script> +<script src="chrome://resources/js/cr.js"></script> +<script src="chrome://resources/js/event_tracker.js"></script> +<script src="chrome://resources/js/cr/event_target.js"></script> +<script src="chrome://resources/js/cr/ui.js"></script> +<script src="chrome://resources/js/cr/ui/touch_handler.js"></script> +<script src="chrome://resources/js/cr/ui/array_data_model.js"></script> +<script src="chrome://resources/js/cr/ui/bubble.js"></script> +<script src="chrome://resources/js/cr/ui/bubble_button.js"></script> +<script src="chrome://resources/js/cr/ui/focus_manager.js"></script> +<script src="chrome://resources/js/cr/ui/focus_outline_manager.js"></script> +<script src="chrome://resources/js/cr/ui/list_selection_model.js"></script> +<script src="chrome://resources/js/cr/ui/list_selection_controller.js"></script> +<script src="chrome://resources/js/cr/ui/list_single_selection_model.js"> +</script> +<script src="chrome://resources/js/cr/ui/list_item.js"></script> +<script src="chrome://resources/js/cr/ui/list.js"></script> +<script src="chrome://resources/js/cr/ui/menu_item.js"></script> +<script src="chrome://resources/js/cr/ui/menu.js"></script> +<script src="chrome://resources/js/cr/ui/autocomplete_list.js"></script> +<script src="chrome://resources/js/cr/ui/grid.js"></script> +<script src="chrome://resources/js/cr/ui/overlay.js"></script> +<script src="chrome://resources/js/cr/ui/position_util.js"></script> +<script src="chrome://resources/js/cr/ui/repeating_button.js"></script> +<script src="chrome://resources/js/cr/ui/tree.js"></script> +<script src="chrome://resources/js/load_time_data.js"></script> +<script src="chrome://resources/js/util.js"></script> + +<script src="chrome://settings-frame/strings.js"></script> +<script src="chrome://settings-frame/options_bundle.js"></script> +</head> + +<body class="uber-frame" + i18n-values=".style.fontFamily:fontfamily;.style.fontSize:fontsize"> +<div id="overlay-container-1" class="overlay transparent" hidden> + <include src="autofill_options.html"> + <include src="clear_browser_data_overlay.html"> + <include src="content_settings.html"> + <include src="content_settings2.html"> + <include src="do_not_track_confirm_overlay.html"> + <include src="font_settings.html"> + <include src="home_page_overlay.html"> + <include src="import_data_overlay.html"> + <include src="language_options.html"> + <include src="manage_profile_overlay.html"> + <include src="managed_user_create_confirm.html"> + <include src="managed_user_import.html"> + <include src="password_manager.html"> + <include src="reset_profile_settings_overlay.html"> + <include src="search_engine_manager.html"> + <include src="spelling_confirm_overlay.html"> + <include src="startup_overlay.html"> + <include src="../sync_setup_overlay.html"> +<if expr="pp_ifdef('chromeos')"> + <include src="chromeos/accounts_options.html"> + <include src="chromeos/bluetooth_add_device_overlay.html"> + <include src="chromeos/bluetooth_pair_device_overlay.html"> + <include src="chromeos/change_picture_options.html"> + <include src="chromeos/display_options.html"> + <include src="chromeos/keyboard_overlay.html"> + <include src="chromeos/pointer_overlay.html"> + <include src="factory_reset_overlay.html"> +</if> +<if expr="pp_ifdef('use_nss')"> + <include src="certificate_manager.html"> +</if> +</div> +<div id="overlay-container-2" class="overlay transparent" hidden> + <include src="alert_overlay.html"> + <include src="autofill_edit_address_overlay.html"> + <include src="autofill_edit_creditcard_overlay.html"> + <include src="content_settings_exceptions_area.html"> + <include src="cookies_view.html"> + <include src="handler_options.html"> + <include src="language_add_language_overlay.html"> + <include src="managed_user_learn_more.html"> +<if expr="not is_macosx"> + <include src="language_dictionary_overlay.html"> +</if> + <include src="media_galleries_manager_overlay.html"> +<if expr="pp_ifdef('chromeos')"> + <include src="chromeos/display_overscan.html"> + <include src="chromeos/internet_detail.html"> + <include src="chromeos/preferred_networks.html"> +</if> +<if expr="not is_win and not is_macosx"> + <include src="certificate_restore_overlay.html"> + <include src="certificate_backup_overlay.html"> + <include src="certificate_edit_ca_trust_overlay.html"> + <include src="certificate_import_error_overlay.html"> +</if> +</div> +<div id="overlay-container-3" class="overlay transparent" hidden> +</div> +<div id="extension-controlled-settings-bubble-template" hidden> + <div class="controlled-setting-bubble-content-row"> + <div class="controlled-setting-bubble-extension-name"></div> + </div> + <div class="controlled-setting-bubble-content-row"> + <div class="controlled-setting-bubble-extension-manage-link link-button" + i18n-content="controlledSettingManageExtensions"></div> + <button class='controlled-setting-bubble-extension-disable-button' + i18n-content="controlledSettingDisableExtension"></button> + </div> +</div> + +<div id="main-content"> + <div id="mainview"> + <div id="mainview-content"> + <div id="page-container"> + <!-- Please keep the main pages in desired order of display. This will + allow search results to display in the desired order. --> + <include src="search_box.html"> + <include src="search_page.html"> + <include src="browser_options.html"> + </div> + </div> + </div> +</div> + +<script src="chrome://resources/js/i18n_template2.js"></script> +</body> +</html> diff --git a/chromium/chrome/browser/resources/options/options.js b/chromium/chrome/browser/resources/options/options.js new file mode 100644 index 00000000000..1aeb352c218 --- /dev/null +++ b/chromium/chrome/browser/resources/options/options.js @@ -0,0 +1,259 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +var AddLanguageOverlay = options.AddLanguageOverlay; +var AlertOverlay = options.AlertOverlay; +var AutofillEditAddressOverlay = options.AutofillEditAddressOverlay; +var AutofillEditCreditCardOverlay = options.AutofillEditCreditCardOverlay; +var AutofillOptions = options.AutofillOptions; +var BrowserOptions = options.BrowserOptions; +var ClearBrowserDataOverlay = options.ClearBrowserDataOverlay; +var ConfirmDialog = options.ConfirmDialog; +var ContentSettingsExceptionsArea = + options.contentSettings.ContentSettingsExceptionsArea; +var ContentSettings = options.ContentSettings; +var CookiesView = options.CookiesView; +var CreateProfileOverlay = options.CreateProfileOverlay; +var EditDictionaryOverlay = cr.IsMac ? null : options.EditDictionaryOverlay; +var FactoryResetOverlay = options.FactoryResetOverlay; +<if expr="pp_ifdef('enable_google_now')"> +var GeolocationOptions = options.GeolocationOptions; +</if> +var FontSettings = options.FontSettings; +var HandlerOptions = options.HandlerOptions; +var HomePageOverlay = options.HomePageOverlay; +var ImportDataOverlay = options.ImportDataOverlay; +var LanguageOptions = options.LanguageOptions; +var ManageProfileOverlay = options.ManageProfileOverlay; +var ManagedUserCreateConfirmOverlay = options.ManagedUserCreateConfirmOverlay; +var ManagedUserImportOverlay = options.ManagedUserImportOverlay; +var ManagedUserLearnMoreOverlay = options.ManagedUserLearnMoreOverlay; +var MediaGalleriesManager = options.MediaGalleriesManager; +var OptionsFocusManager = options.OptionsFocusManager; +var OptionsPage = options.OptionsPage; +var PasswordManager = options.PasswordManager; +var Preferences = options.Preferences; +var PreferredNetworks = options.PreferredNetworks; +var ResetProfileSettingsBanner = options.ResetProfileSettingsBanner; +var ResetProfileSettingsOverlay = options.ResetProfileSettingsOverlay; +var SearchEngineManager = options.SearchEngineManager; +var SearchPage = options.SearchPage; +var StartupOverlay = options.StartupOverlay; +var SyncSetupOverlay = options.SyncSetupOverlay; + +/** + * DOMContentLoaded handler, sets up the page. + */ +function load() { + // Decorate the existing elements in the document. + cr.ui.decorate('input[pref][type=checkbox]', options.PrefCheckbox); + cr.ui.decorate('input[pref][type=number]', options.PrefNumber); + cr.ui.decorate('input[pref][type=radio]', options.PrefRadio); + cr.ui.decorate('input[pref][type=range]', options.PrefRange); + cr.ui.decorate('select[pref]', options.PrefSelect); + cr.ui.decorate('input[pref][type=text]', options.PrefTextField); + cr.ui.decorate('input[pref][type=url]', options.PrefTextField); + cr.ui.decorate('button[pref]', options.PrefButton); + cr.ui.decorate('#content-settings-page input[type=radio]:not(.handler-radio)', + options.ContentSettingsRadio); + cr.ui.decorate('#content-settings-page input[type=radio].handler-radio', + options.HandlersEnabledRadio); + cr.ui.decorate('span.controlled-setting-indicator', + options.ControlledSettingIndicator); + + // Top level pages. + OptionsPage.register(SearchPage.getInstance()); + OptionsPage.register(BrowserOptions.getInstance()); + + // Overlays. + OptionsPage.registerOverlay(AddLanguageOverlay.getInstance(), + LanguageOptions.getInstance()); + OptionsPage.registerOverlay(AlertOverlay.getInstance()); + OptionsPage.registerOverlay(AutofillEditAddressOverlay.getInstance(), + AutofillOptions.getInstance()); + OptionsPage.registerOverlay(AutofillEditCreditCardOverlay.getInstance(), + AutofillOptions.getInstance()); + OptionsPage.registerOverlay(AutofillOptions.getInstance(), + BrowserOptions.getInstance(), + [$('autofill-settings')]); + OptionsPage.registerOverlay(ClearBrowserDataOverlay.getInstance(), + BrowserOptions.getInstance(), + [$('privacyClearDataButton')]); + OptionsPage.registerOverlay( + new ConfirmDialog( + 'doNotTrackConfirm', + loadTimeData.getString('doNotTrackConfirmOverlayTabTitle'), + 'do-not-track-confirm-overlay', + $('do-not-track-confirm-ok'), + $('do-not-track-confirm-cancel'), + $('do-not-track-enabled').pref, + $('do-not-track-enabled').metric), + BrowserOptions.getInstance()); + // 'spelling-enabled-control' element is only present on Chrome branded + // builds. + if ($('spelling-enabled-control')) { + OptionsPage.registerOverlay( + new ConfirmDialog( + 'spellingConfirm', + loadTimeData.getString('spellingConfirmOverlayTabTitle'), + 'spelling-confirm-overlay', + $('spelling-confirm-ok'), + $('spelling-confirm-cancel'), + $('spelling-enabled-control').pref, + $('spelling-enabled-control').metric), + BrowserOptions.getInstance()); + } + OptionsPage.registerOverlay(ContentSettings.getInstance(), + BrowserOptions.getInstance(), + [$('privacyContentSettingsButton')]); + OptionsPage.registerOverlay(ContentSettingsExceptionsArea.getInstance(), + ContentSettings.getInstance()); + OptionsPage.registerOverlay(CookiesView.getInstance(), + ContentSettings.getInstance(), + [$('privacyContentSettingsButton'), + $('show-cookies-button')]); + OptionsPage.registerOverlay(CreateProfileOverlay.getInstance(), + BrowserOptions.getInstance()); + if (!cr.isMac) { + OptionsPage.registerOverlay(EditDictionaryOverlay.getInstance(), + LanguageOptions.getInstance(), + [$('edit-dictionary-button')]); + } + OptionsPage.registerOverlay(FontSettings.getInstance(), + BrowserOptions.getInstance(), + [$('fontSettingsCustomizeFontsButton')]); + if (HandlerOptions && $('manage-handlers-button')) { + OptionsPage.registerOverlay(HandlerOptions.getInstance(), + ContentSettings.getInstance(), + [$('manage-handlers-button')]); + } + OptionsPage.registerOverlay(HomePageOverlay.getInstance(), + BrowserOptions.getInstance(), + [$('change-home-page')]); + OptionsPage.registerOverlay(ImportDataOverlay.getInstance(), + BrowserOptions.getInstance()); + OptionsPage.registerOverlay(LanguageOptions.getInstance(), + BrowserOptions.getInstance(), + [$('language-button'), + $('manage-languages')]); + OptionsPage.registerOverlay(ManageProfileOverlay.getInstance(), + BrowserOptions.getInstance()); + if (!cr.isChromeOS) { + OptionsPage.registerOverlay(ManagedUserCreateConfirmOverlay.getInstance(), + BrowserOptions.getInstance()); + if (loadTimeData.getBoolean('allowCreateExistingManagedUsers')) { + OptionsPage.registerOverlay(ManagedUserImportOverlay.getInstance(), + BrowserOptions.getInstance()); + } + OptionsPage.registerOverlay(ManagedUserLearnMoreOverlay.getInstance(), + CreateProfileOverlay.getInstance()); + } + OptionsPage.registerOverlay(MediaGalleriesManager.getInstance(), + ContentSettings.getInstance(), + [$('manage-galleries-button')]); + OptionsPage.registerOverlay(PasswordManager.getInstance(), + BrowserOptions.getInstance(), + [$('manage-passwords')]); + OptionsPage.registerOverlay(ResetProfileSettingsOverlay.getInstance(), + BrowserOptions.getInstance(), + [$('reset-profile-settings')]); + OptionsPage.registerOverlay(SearchEngineManager.getInstance(), + BrowserOptions.getInstance(), + [$('manage-default-search-engines')]); + OptionsPage.registerOverlay(StartupOverlay.getInstance(), + BrowserOptions.getInstance()); + OptionsPage.registerOverlay(SyncSetupOverlay.getInstance(), + BrowserOptions.getInstance(), + [$('customize-sync')]); + if (cr.isChromeOS) { + OptionsPage.registerOverlay(AccountsOptions.getInstance(), + BrowserOptions.getInstance(), + [$('manage-accounts-button')]); + OptionsPage.registerOverlay(BluetoothOptions.getInstance(), + BrowserOptions.getInstance(), + [$('bluetooth-add-device')]); + OptionsPage.registerOverlay(BluetoothPairing.getInstance(), + BrowserOptions.getInstance()); + OptionsPage.registerOverlay(FactoryResetOverlay.getInstance(), + BrowserOptions.getInstance(), + [$('factory-reset-restart')]); + OptionsPage.registerOverlay(ChangePictureOptions.getInstance(), + BrowserOptions.getInstance(), + [$('account-picture')]); + OptionsPage.registerOverlay(DetailsInternetPage.getInstance(), + BrowserOptions.getInstance()); + OptionsPage.registerOverlay(DisplayOptions.getInstance(), + BrowserOptions.getInstance(), + [$('display-options')]); + OptionsPage.registerOverlay(DisplayOverscan.getInstance(), + DisplayOptions.getInstance()); + OptionsPage.registerOverlay(KeyboardOverlay.getInstance(), + BrowserOptions.getInstance(), + [$('keyboard-settings-button')]); + OptionsPage.registerOverlay(PointerOverlay.getInstance(), + BrowserOptions.getInstance(), + [$('pointer-settings-button')]); + OptionsPage.registerOverlay(PreferredNetworks.getInstance(), + BrowserOptions.getInstance()); + } + + if (!cr.isWindows && !cr.isMac) { + OptionsPage.registerOverlay(CertificateBackupOverlay.getInstance(), + CertificateManager.getInstance()); + OptionsPage.registerOverlay(CertificateEditCaTrustOverlay.getInstance(), + CertificateManager.getInstance()); + OptionsPage.registerOverlay(CertificateImportErrorOverlay.getInstance(), + CertificateManager.getInstance()); + OptionsPage.registerOverlay(CertificateManager.getInstance(), + BrowserOptions.getInstance(), + [$('certificatesManageButton')]); + OptionsPage.registerOverlay(CertificateRestoreOverlay.getInstance(), + CertificateManager.getInstance()); + } + + cr.ui.FocusManager.disableMouseFocusOnButtons(); + OptionsFocusManager.getInstance().initialize(); + Preferences.getInstance().initialize(); + ResetProfileSettingsBanner.getInstance().initialize(); + OptionsPage.initialize(); + + var path = document.location.pathname; + + if (path.length > 1) { + // Skip starting slash and remove trailing slash (if any). + var pageName = path.slice(1).replace(/\/$/, ''); + OptionsPage.showPageByName(pageName, true, {replaceState: true}); + } else { + OptionsPage.showDefaultPage(); + } + + var subpagesNavTabs = document.querySelectorAll('.subpages-nav-tabs'); + for (var i = 0; i < subpagesNavTabs.length; i++) { + subpagesNavTabs[i].onclick = function(event) { + OptionsPage.showTab(event.srcElement); + }; + } + + window.setTimeout(function() { + document.documentElement.classList.remove('loading'); + }); +} + +document.documentElement.classList.add('loading'); +document.addEventListener('DOMContentLoaded', load); + +/** + * Listener for the |beforeunload| event. + */ +window.onbeforeunload = function() { + options.OptionsPage.willClose(); +}; + +/** + * Listener for the |popstate| event. + * @param {Event} e The |popstate| event. + */ +window.onpopstate = function(e) { + options.OptionsPage.setState(e.state); +}; diff --git a/chromium/chrome/browser/resources/options/options_bundle.js b/chromium/chrome/browser/resources/options/options_bundle.js new file mode 100644 index 00000000000..8f450648e5a --- /dev/null +++ b/chromium/chrome/browser/resources/options/options_bundle.js @@ -0,0 +1,117 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// This file exists to aggregate all of the javascript used by the +// settings page into a single file which will be flattened and served +// as a single resource. +<include src="preferences.js"></include> +<include src="controlled_setting.js"></include> +<include src="deletable_item_list.js"></include> +<include src="editable_text_field.js"></include> +<include src="inline_editable_list.js"></include> +<include src="options_page.js"></include> +<include src="pref_ui.js"></include> +<include src="settings_dialog.js"></include> +<if expr="pp_ifdef('chromeos')"> +<include src="../chromeos/user_images_grid.js"></include> +// DO NOT BREAK THE FOLLOWING INCLUDE LINE INTO SEPARATE LINES! +// Even though the include line spans more than 80 characters, +// The grit html inlining parser will leave the end tag behind, +// causing a runtime JS break. +<include src="../../../../ui/webui/resources/js/chromeos/ui_account_tweaks.js"></include> +<include src="chromeos/change_picture_options.js"></include> +<include src="chromeos/internet_detail_ip_address_field.js"></include> +<include src="chromeos/internet_detail.js"></include> +<include src="chromeos/network_list.js"></include> +<include src="chromeos/preferred_networks.js"></include> +<include src="chromeos/bluetooth_device_list.js"></include> +<include src="chromeos/bluetooth_add_device_overlay.js"></include> +<include src="chromeos/bluetooth_pair_device_overlay.js"></include> +<include src="chromeos/accounts_options.js"></include> +<include src="chromeos/proxy_rules_list.js"></include> +<include src="chromeos/accounts_user_list.js"></include> +<include src="chromeos/accounts_user_name_edit.js"></include> +<include src="chromeos/display_options.js"></include> +<include src="chromeos/display_overscan.js"></include> +<include src="chromeos/keyboard_overlay.js"></include> +<include src="chromeos/pointer_overlay.js"></include> +var AccountsOptions = options.AccountsOptions; +var ChangePictureOptions = options.ChangePictureOptions; +var DetailsInternetPage = options.internet.DetailsInternetPage; +var DisplayOptions = options.DisplayOptions; +var DisplayOverscan = options.DisplayOverscan; +var BluetoothOptions = options.BluetoothOptions; +var BluetoothPairing = options.BluetoothPairing; +var KeyboardOverlay = options.KeyboardOverlay; +var PointerOverlay = options.PointerOverlay; +var UIAccountTweaks = uiAccountTweaks.UIAccountTweaks; +</if> +<if expr="pp_ifdef('use_nss')"> +<include src="certificate_tree.js"></include> +<include src="certificate_manager.js"></include> +<include src="certificate_restore_overlay.js"></include> +<include src="certificate_backup_overlay.js"></include> +<include src="certificate_edit_ca_trust_overlay.js"></include> +<include src="certificate_import_error_overlay.js"></include> +var CertificateManager = options.CertificateManager; +var CertificateRestoreOverlay = options.CertificateRestoreOverlay; +var CertificateBackupOverlay = options.CertificateBackupOverlay; +var CertificateEditCaTrustOverlay = options.CertificateEditCaTrustOverlay; +var CertificateImportErrorOverlay = options.CertificateImportErrorOverlay; +</if> +<include src="alert_overlay.js"></include> +<include src="autofill_edit_address_overlay.js"></include> +<include src="autofill_edit_creditcard_overlay.js"></include> +<include src="autofill_options_list.js"></include> +<include src="autofill_options.js"></include> +<include src="browser_options.js"></include> +<include src="browser_options_profile_list.js"></include> +<include src="browser_options_startup_page_list.js"></include> +<include src="clear_browser_data_overlay.js"></include> +<include src="confirm_dialog.js"></include> +<include src="content_settings.js"></include> +<include src="content_settings2.js"></include> +<include src="content_settings_exceptions_area.js"></include> +<include src="content_settings_ui.js"></include> +<include src="cookies_list.js"></include> +<include src="cookies_view.js"></include> +<include src="factory_reset_overlay.js"></include> +<include src="font_settings.js"></include> +<if expr="pp_ifdef('enable_google_now')"> +<include src="geolocation_options.js"></include> +</if> +<include src="handler_options.js"></include> +<include src="handler_options_list.js"></include> +<include src="home_page_overlay.js"></include> +<include src="import_data_overlay.js"></include> +<include src="language_add_language_overlay.js"></include> +<if expr="not is_macosx"> +<include src="language_dictionary_overlay_word_list.js"></include> +<include src="language_dictionary_overlay.js"></include> +</if> +<include src="language_list.js"></include> +<include src="language_options.js"></include> +<include src="manage_profile_overlay.js"></include> +<include src="managed_user_create_confirm.js"</include> +<include src="managed_user_import.js"></include> +<include src="managed_user_learn_more.js"</include> +<include src="managed_user_list.js"></include> +<include src="media_galleries_list.js"></include> +<include src="media_galleries_manager_overlay.js"></include> +<include src="options_focus_manager.js"></include> +<include src="password_manager.js"></include> +<include src="password_manager_list.js"></include> +<include src="profiles_icon_grid.js"></include> +<include src="reset_profile_settings_banner.js"></include> +<include src="reset_profile_settings_overlay.js"></include> +<include src="search_engine_manager.js"></include> +<include src="search_engine_manager_engine_list.js"></include> +<include src="search_page.js"></include> +<include src="startup_overlay.js"></include> +<include src="../sync_setup_overlay.js"></include> +<include src="../uber/uber_utils.js"></include> +<include src="options.js"></include> +<if expr="pp_ifdef('enable_settings_app')"> +<include src="options_settings_app.js"></include> +</if> diff --git a/chromium/chrome/browser/resources/options/options_focus_manager.js b/chromium/chrome/browser/resources/options/options_focus_manager.js new file mode 100644 index 00000000000..972e1fea0db --- /dev/null +++ b/chromium/chrome/browser/resources/options/options_focus_manager.js @@ -0,0 +1,34 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + var FocusManager = cr.ui.FocusManager; + var OptionsPage = options.OptionsPage; + + function OptionsFocusManager() { + } + + cr.addSingletonGetter(OptionsFocusManager); + + OptionsFocusManager.prototype = { + __proto__: FocusManager.prototype, + + /** @override */ + getFocusParent: function() { + var topPage = OptionsPage.getTopmostVisiblePage().pageDiv; + + // The default page and search page include a search field that is a + // sibling of the rest of the page instead of a child. Thus, use the + // parent node to allow the search field to receive focus. + if (topPage.parentNode.id == 'page-container') + return topPage.parentNode; + + return topPage; + }, + }; + + return { + OptionsFocusManager: OptionsFocusManager, + }; +}); diff --git a/chromium/chrome/browser/resources/options/options_page.css b/chromium/chrome/browser/resources/options/options_page.css new file mode 100644 index 00000000000..b4223ec7e32 --- /dev/null +++ b/chromium/chrome/browser/resources/options/options_page.css @@ -0,0 +1,459 @@ +/* Copyright (c) 2012 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ + +body { + position: relative; +} + +#main-content { + bottom: 0; + display: -webkit-box; + left: 0; + position: absolute; + right: 0; + top: 0; +} + +#mainview { + -webkit-box-align: stretch; + bottom: 0; + left: 0; + margin: 0; + position: absolute; + right: 0; + top: 0; + z-index: 1; +} + +#mainview-content { + min-height: 100%; + position: relative; +} + +#page-container { + box-sizing: border-box; + max-width: 888px; + min-width: 600px; +} + +body.uber-frame #searchBox { + position: fixed; + z-index: 4; +} + +div.disabled { + color: #999; +} + +.settings-row { + display: block; + margin: 0.65em 0; +} + +.hbox { + -webkit-box-orient: horizontal; + display: -webkit-box; +} + +.vbox { + -webkit-box-orient: vertical; + display: -webkit-box; +} + +.box-align-center { + -webkit-box-align: center; +} + +.stretch { + -webkit-box-flex: 1; +} + +.frozen { + position: fixed; +} + +#overlay-container-1 { + z-index: 11; +} +#overlay-container-2 { + z-index: 12; +} +#overlay-container-3 { + z-index: 13; +} + +.raw-button, +.raw-button:hover, +.raw-button:active { + -webkit-box-shadow: none; + background-color: transparent; + background-repeat: no-repeat; + border: none; + min-width: 0; + padding: 1px 6px; +} + +.bottom-strip { + border-top: none; + bottom: 0; + padding: 12px; + position: absolute; + right: 0; +} + +/* Omit top padding (currently only on #settings) whenever the search page is + * showing. + */ +#searchPage:not([hidden]) + #settings { + padding-top: 0; +} + +.page list { + /* Min height is a multiple of the list item height (32) */ + min-height: 192px; +} + +.option { + margin-top: 0; +} + +.transparent { + opacity: 0; +} + +.touch-slider { + -webkit-appearance: slider-horizontal; +} + +.settings-list, +.settings-list-empty { + border: 1px solid #d9d9d9; + border-radius: 2px; +} + +.settings-list-empty { + background-color: #f4f4f4; + box-sizing: border-box; + min-height: 125px; + padding-left: 20px; + padding-top: 20px; +} + + +/* Editable text field properties */ +.editable-text-field > * { + -webkit-box-align: center; + -webkit-transition: 150ms background-color; + border: none; + box-sizing: border-box; + display: -webkit-box; + height: 20px; + margin: 0; +} + +.editable-text-field > .spacer { + /* The above height rule should not apply to spacers. */ + height: 0; +} + +.editable-text-field .editable-text { + padding: 2px 3px; +} + +.editable-text-field .static-text { + height: 24px; + overflow: hidden; + padding: 3px 4px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.editable-text-field:not([editable]) > [displaymode='edit'] { + display: none; +} + +.editable-text-field[editable] > [displaymode='static'] { + display: none; +} + +.editable-text-field[empty] > input[type='text'] { + color: #ccc; + font-style: italic; +} + +.editable-text-field[disabled] { + opacity: 0.6; +} + +/* Editable List properties */ +list > * { + -webkit-box-align: center; + -webkit-transition: 150ms background-color; + border: none; + border-radius: 0; /* TODO(dbeam): Is this necessary? */ + box-sizing: border-box; + display: -webkit-box; + height: 32px; + margin: 0; +} + +list > .spacer { + /* The above height rule should not apply to spacers. When redraw is called + on the list they will be given an explicit element height but this ensures + they have 0 height to begin with. */ + height: 0; +} + +list:not([disabled]) > :hover { + background-color: rgb(228, 236, 247); +} + +/* TODO(stuartmorgan): Once this becomes the list style for other WebUI pages + * these rules can be simplified (since they wont need to override other rules). + */ + +list:not([hasElementFocus]) > [selected], +list:not([hasElementFocus]) > [lead][selected] { + background-color: #d0d0d0; + background-image: none; +} + +list[hasElementFocus] > [selected], +list[hasElementFocus] > [lead][selected], +list:not([hasElementFocus]) > [selected]:hover, +list:not([hasElementFocus]) > [selected][lead]:hover { + background-color: rgb(187, 206, 233); + background-image: none; +} + +list[hasElementFocus] > [lead], +list[hasElementFocus] > [lead][selected] { + border-bottom: 1px solid rgb(120, 146, 180); + border-top: 1px solid rgb(120, 146, 180); +} + +list[hasElementFocus] > [lead]:nth-child(2), +list[hasElementFocus] > [lead][selected]:nth-child(2) { + border-top: 1px solid transparent; +} + +list[hasElementFocus] > [lead]:nth-last-child(2), +list[hasElementFocus] > [lead][selected]:nth-last-child(2) { + border-bottom: 1px solid transparent; +} + +list[disabled] > [lead][selected], +list[disabled]:focus > [lead][selected] { + border: none; +} + +list[disabled] { + opacity: 0.6; +} + +list > .heading { + color: #666; +} + +list > .heading:hover { + background-color: transparent; + border-color: transparent; +} + +list .deletable-item { + -webkit-box-align: center; +} + +list .deletable-item > :first-child { + -webkit-box-align: center; + -webkit-box-flex: 1; + -webkit-padding-end: 5px; + display: -webkit-box; +} + +list .row-delete-button { + -webkit-transition: 150ms opacity; + background-color: transparent; + /* TODO(stuartmorgan): Replace with real images once they are available. */ + background-image: -webkit-image-set( + url('../../../../ui/resources/default_100_percent/close_2.png') 1x, + url('../../../../ui/resources/default_200_percent/close_2.png') 2x); + border: none; + display: block; + height: 16px; + opacity: 1; + width: 16px; +} + +list > *:not(:hover):not([selected]):not([lead]) .row-delete-button, +list:not([hasElementFocus]) > *:not(:hover):not([selected]) .row-delete-button, +list[disabled] .row-delete-button, +list .row-delete-button[disabled] { + opacity: 0; + pointer-events: none; +} + +/* HostedApp entries use the disabled closing button to display the App's + * favicon, as an indicator that instead of deleting the permission here + * the user has to remove the hosted app.*/ +list div[role='listitem'][managedby='HostedApp'] .row-delete-button { + opacity: 1; +} + +list .row-delete-button:hover { + background-image: -webkit-image-set( + url('../../../../ui/resources/default_100_percent/close_2_hover.png') 1x, + url('../../../../ui/resources/default_200_percent/close_2_hover.png') 2x); +} + +list .row-delete-button:active { + background-image: -webkit-image-set( + url('../../../../ui/resources/default_100_percent/close_2_pressed.png') + 1x, + url('../../../../ui/resources/default_200_percent/close_2_pressed.png') + 2x); +} + +list .static-text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +list[type='text'][inlineeditable] input { + box-sizing: border-box; + margin: 0; + width: 100%; +} + +list > :not([editing]) [displaymode='edit'] { + display: none; +} + +list > [editing] [displaymode='static'] { + /* Don't use display:none because we need to keep an element focusable. */ + left: 0; + opacity: 0; + pointer-events: none; + position: absolute; + top: -10em; +} + +list > [editing] input:invalid { + /* TODO(stuartmorgan): Replace with validity badge */ + background-color: pink; +} + +.list-inline-button { + -webkit-appearance: none; + -webkit-transition: opacity 150ms; + background: rgb(138, 170, 237); + border: none; + border-radius: 2px; + color: white; + font-weight: bold; + opacity: 0.7; +} + +.list-inline-button:hover { + opacity: 1; +} + +.option-name { + padding-right: 5px; +} + +html[dir=rtl].option-name { + padding-left: 5px; +} + +.favicon-cell { + -webkit-padding-start: 20px; + background-position: left; + background-repeat: no-repeat; + background-size: 16px; +} + +input[type='url'].favicon-cell { + -webkit-padding-start: 22px; + background-position-x: 4px; +} + +html[dir=rtl] input.favicon-cell { + background-position-x: -webkit-calc(100% - 4px); +} + +list .favicon-cell { + -webkit-margin-start: 7px; + -webkit-padding-start: 26px; + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +html[dir=rtl] list .favicon-cell { + background-position: right; +} + +html[enable-background-mode=false] #background-mode-section { + display: none; +} + +/* UI Controls */ + +/* LIST */ +list[hasElementFocus] { +<if expr="not is_macosx"> + outline: 1px solid rgba(0, 128, 256, 0.5); + outline-offset: -2px; +</if> +<if expr="is_macosx"> + /* This matches the native list outline on Mac */ + outline-color: rgb(117, 154, 217); + outline-offset: -1px; + outline-style: auto; + outline-width: 5px; +</if> +} + +.suboption { + -webkit-margin-start: 23px; +} + +list.autocomplete-suggestions { + background-color: white; + border: 1px solid #aaa; + border-radius: 2px; + min-height: 0; + opacity: 0.9; + position: fixed; + z-index: 3; +} + +list.autocomplete-suggestions > div { + height: auto; +} + +list.autocomplete-suggestions:not([hasElementFocus]) > [selected], +list.autocomplete-suggestions:not([hasElementFocus]) > [lead][selected] { + background-color: rgb(187, 206, 233); +} + +html:not(.focus-outline-visible) +:enabled:focus:-webkit-any(input[type='checkbox'], input[type='radio']) { + /* Cancel border-color for :focus specified in widgets.css. */ + border-color: rgba(0, 0, 0, 0.25); +} + +html:not([hasFlashPlugin]) .flash-plugin-area, +/* If the Flash plug-in supports the NPP_ClearSiteData API, we don't need to + * show the link to the Flash storage settings manager: + */ +html[flashPluginSupportsClearSiteData] .flash-plugin-area, +html:not([flashPluginSupportsClearSiteData]) .clear-plugin-lso-data-enabled, +html[flashPluginSupportsClearSiteData] .clear-plugin-lso-data-disabled, +html:not([enablePepperFlashSettings]) .pepper-flash-settings { + display: none; +} diff --git a/chromium/chrome/browser/resources/options/options_page.js b/chromium/chrome/browser/resources/options/options_page.js new file mode 100644 index 00000000000..befb7c81398 --- /dev/null +++ b/chromium/chrome/browser/resources/options/options_page.js @@ -0,0 +1,1006 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + /** @const */ var FocusOutlineManager = cr.ui.FocusOutlineManager; + + ///////////////////////////////////////////////////////////////////////////// + // OptionsPage class: + + /** + * Base class for options page. + * @constructor + * @param {string} name Options page name. + * @param {string} title Options page title, used for history. + * @extends {EventTarget} + */ + function OptionsPage(name, title, pageDivName) { + this.name = name; + this.title = title; + this.pageDivName = pageDivName; + this.pageDiv = $(this.pageDivName); + // |pageDiv.page| is set to the page object (this) when the page is visible + // to track which page is being shown when multiple pages can share the same + // underlying div. + this.pageDiv.page = null; + this.tab = null; + this.lastFocusedElement = null; + } + + /** + * This is the absolute difference maintained between standard and + * fixed-width font sizes. Refer http://crbug.com/91922. + * @const + */ + OptionsPage.SIZE_DIFFERENCE_FIXED_STANDARD = 3; + + /** + * Offset of page container in pixels, to allow room for side menu. + * Simplified settings pages can override this if they don't use the menu. + * The default (155) comes from -webkit-margin-start in uber_shared.css + * @private + */ + OptionsPage.horizontalOffset = 155; + + /** + * Main level option pages. Maps lower-case page names to the respective page + * object. + * @protected + */ + OptionsPage.registeredPages = {}; + + /** + * Pages which are meant to behave like modal dialogs. Maps lower-case overlay + * names to the respective overlay object. + * @protected + */ + OptionsPage.registeredOverlayPages = {}; + + /** + * Gets the default page (to be shown on initial load). + */ + OptionsPage.getDefaultPage = function() { + return BrowserOptions.getInstance(); + }; + + /** + * Shows the default page. + */ + OptionsPage.showDefaultPage = function() { + this.navigateToPage(this.getDefaultPage().name); + }; + + /** + * "Navigates" to a page, meaning that the page will be shown and the + * appropriate entry is placed in the history. + * @param {string} pageName Page name. + */ + OptionsPage.navigateToPage = function(pageName) { + this.showPageByName(pageName, true); + }; + + /** + * Shows a registered page. This handles both top-level and overlay pages. + * @param {string} pageName Page name. + * @param {boolean} updateHistory True if we should update the history after + * showing the page. + * @param {Object=} opt_propertyBag An optional bag of properties including + * replaceState (if history state should be replaced instead of pushed). + * @private + */ + OptionsPage.showPageByName = function(pageName, + updateHistory, + opt_propertyBag) { + // If |opt_propertyBag| is non-truthy, homogenize to object. + opt_propertyBag = opt_propertyBag || {}; + + // If a bubble is currently being shown, hide it. + this.hideBubble(); + + // Find the currently visible root-level page. + var rootPage = null; + for (var name in this.registeredPages) { + var page = this.registeredPages[name]; + if (page.visible && !page.parentPage) { + rootPage = page; + break; + } + } + + // Find the target page. + var targetPage = this.registeredPages[pageName.toLowerCase()]; + if (!targetPage || !targetPage.canShowPage()) { + // If it's not a page, try it as an overlay. + if (!targetPage && this.showOverlay_(pageName, rootPage)) { + if (updateHistory) + this.updateHistoryState_(!!opt_propertyBag.replaceState); + return; + } else { + targetPage = this.getDefaultPage(); + } + } + + pageName = targetPage.name.toLowerCase(); + var targetPageWasVisible = targetPage.visible; + + // Determine if the root page is 'sticky', meaning that it + // shouldn't change when showing an overlay. This can happen for special + // pages like Search. + var isRootPageLocked = + rootPage && rootPage.sticky && targetPage.parentPage; + + var allPageNames = Array.prototype.concat.call( + Object.keys(this.registeredPages), + Object.keys(this.registeredOverlayPages)); + + // Notify pages if they will be hidden. + for (var i = 0; i < allPageNames.length; ++i) { + var name = allPageNames[i]; + var page = this.registeredPages[name] || + this.registeredOverlayPages[name]; + if (!page.parentPage && isRootPageLocked) + continue; + if (page.willHidePage && name != pageName && + !page.isAncestorOfPage(targetPage)) { + page.willHidePage(); + } + } + + // Update visibilities to show only the hierarchy of the target page. + for (var i = 0; i < allPageNames.length; ++i) { + var name = allPageNames[i]; + var page = this.registeredPages[name] || + this.registeredOverlayPages[name]; + if (!page.parentPage && isRootPageLocked) + continue; + page.visible = name == pageName || page.isAncestorOfPage(targetPage); + } + + // Update the history and current location. + if (updateHistory) + this.updateHistoryState_(!!opt_propertyBag.replaceState); + + // Update tab title. + this.setTitle_(targetPage.title); + + // Update focus if any other control was focused on the previous page, + // or the previous page is not known. + if (document.activeElement != document.body && + (!rootPage || rootPage.pageDiv.contains(document.activeElement))) { + targetPage.focus(); + } + + // Notify pages if they were shown. + for (var i = 0; i < allPageNames.length; ++i) { + var name = allPageNames[i]; + var page = this.registeredPages[name] || + this.registeredOverlayPages[name]; + if (!page.parentPage && isRootPageLocked) + continue; + if (!targetPageWasVisible && page.didShowPage && + (name == pageName || page.isAncestorOfPage(targetPage))) { + page.didShowPage(); + } + } + }; + + /** + * Sets the title of the page. This is accomplished by calling into the + * parent page API. + * @param {string} title The title string. + * @private + */ + OptionsPage.setTitle_ = function(title) { + uber.invokeMethodOnParent('setTitle', {title: title}); + }; + + /** + * Scrolls the page to the correct position (the top when opening an overlay, + * or the old scroll position a previously hidden overlay becomes visible). + * @private + */ + OptionsPage.updateScrollPosition_ = function() { + var container = $('page-container'); + var scrollTop = container.oldScrollTop || 0; + container.oldScrollTop = undefined; + window.scroll(scrollLeftForDocument(document), scrollTop); + }; + + /** + * Pushes the current page onto the history stack, overriding the last page + * if it is the generic chrome://settings/. + * @param {boolean} replace If true, allow no history events to be created. + * @param {object=} opt_params A bag of optional params, including: + * {boolean} ignoreHash Whether to include the hash or not. + * @private + */ + OptionsPage.updateHistoryState_ = function(replace, opt_params) { + var page = this.getTopmostVisiblePage(); + var path = window.location.pathname + window.location.hash; + if (path) + path = path.slice(1).replace(/\/(?:#|$)/, ''); // Remove trailing slash. + + // Update tab title. + this.setTitle_(page.title); + + // The page is already in history (the user may have clicked the same link + // twice). Do nothing. + if (path == page.name && !OptionsPage.isLoading()) + return; + + var hash = opt_params && opt_params.ignoreHash ? '' : window.location.hash; + + // If settings are embedded, tell the outer page to set its "path" to the + // inner frame's path. + var outerPath = (page == this.getDefaultPage() ? '' : page.name) + hash; + uber.invokeMethodOnParent('setPath', {path: outerPath}); + + // If there is no path, the current location is chrome://settings/. + // Override this with the new page. + var historyFunction = path && !replace ? window.history.pushState : + window.history.replaceState; + historyFunction.call(window.history, + {pageName: page.name}, + page.title, + '/' + page.name + hash); + }; + + /** + * Shows a registered Overlay page. Does not update history. + * @param {string} overlayName Page name. + * @param {OptionPage} rootPage The currently visible root-level page. + * @return {boolean} whether we showed an overlay. + */ + OptionsPage.showOverlay_ = function(overlayName, rootPage) { + var overlay = this.registeredOverlayPages[overlayName.toLowerCase()]; + if (!overlay || !overlay.canShowPage()) + return false; + + // Save the currently focused element in the page for restoration later. + var currentPage = this.getTopmostVisiblePage(); + if (currentPage) + currentPage.lastFocusedElement = document.activeElement; + + if ((!rootPage || !rootPage.sticky) && + overlay.parentPage && + !overlay.parentPage.visible) { + this.showPageByName(overlay.parentPage.name, false); + } + + if (!overlay.visible) { + overlay.visible = true; + if (overlay.didShowPage) overlay.didShowPage(); + } + + // Update tab title. + this.setTitle_(overlay.title); + + // Change focus to the overlay if any other control was focused by keyboard + // before. Otherwise, no one should have focus. + if (document.activeElement != document.body) { + if (FocusOutlineManager.forDocument(document).visible) { + overlay.focus(); + } else if (!overlay.pageDiv.contains(document.activeElement)) { + document.activeElement.blur(); + } + } + + if ($('search-field').value == '') { + var section = overlay.associatedSection; + if (section) + options.BrowserOptions.scrollToSection(section); + } + + return true; + }; + + /** + * Returns whether or not an overlay is visible. + * @return {boolean} True if an overlay is visible. + * @private + */ + OptionsPage.isOverlayVisible_ = function() { + return this.getVisibleOverlay_() != null; + }; + + /** + * Returns the currently visible overlay, or null if no page is visible. + * @return {OptionPage} The visible overlay. + */ + OptionsPage.getVisibleOverlay_ = function() { + var topmostPage = null; + for (var name in this.registeredOverlayPages) { + var page = this.registeredOverlayPages[name]; + if (page.visible && + (!topmostPage || page.nestingLevel > topmostPage.nestingLevel)) { + topmostPage = page; + } + } + return topmostPage; + }; + + /** + * Restores the last focused element on a given page. + */ + OptionsPage.restoreLastFocusedElement_ = function() { + var currentPage = this.getTopmostVisiblePage(); + if (currentPage.lastFocusedElement) + currentPage.lastFocusedElement.focus(); + }; + + /** + * Closes the visible overlay. Updates the history state after closing the + * overlay. + */ + OptionsPage.closeOverlay = function() { + var overlay = this.getVisibleOverlay_(); + if (!overlay) + return; + + overlay.visible = false; + + if (overlay.didClosePage) overlay.didClosePage(); + this.updateHistoryState_(false, {ignoreHash: true}); + + this.restoreLastFocusedElement_(); + }; + + /** + * Cancels (closes) the overlay, due to the user pressing <Esc>. + */ + OptionsPage.cancelOverlay = function() { + // Blur the active element to ensure any changed pref value is saved. + document.activeElement.blur(); + var overlay = this.getVisibleOverlay_(); + // Let the overlay handle the <Esc> if it wants to. + if (overlay.handleCancel) { + overlay.handleCancel(); + this.restoreLastFocusedElement_(); + } else { + this.closeOverlay(); + } + }; + + /** + * Hides the visible overlay. Does not affect the history state. + * @private + */ + OptionsPage.hideOverlay_ = function() { + var overlay = this.getVisibleOverlay_(); + if (overlay) + overlay.visible = false; + }; + + /** + * Returns the pages which are currently visible, ordered by nesting level + * (ascending). + * @return {Array.OptionPage} The pages which are currently visible, ordered + * by nesting level (ascending). + */ + OptionsPage.getVisiblePages_ = function() { + var visiblePages = []; + for (var name in this.registeredPages) { + var page = this.registeredPages[name]; + if (page.visible) + visiblePages[page.nestingLevel] = page; + } + return visiblePages; + }; + + /** + * Returns the topmost visible page (overlays excluded). + * @return {OptionPage} The topmost visible page aside any overlay. + * @private + */ + OptionsPage.getTopmostVisibleNonOverlayPage_ = function() { + var topPage = null; + for (var name in this.registeredPages) { + var page = this.registeredPages[name]; + if (page.visible && + (!topPage || page.nestingLevel > topPage.nestingLevel)) + topPage = page; + } + + return topPage; + }; + + /** + * Returns the topmost visible page, or null if no page is visible. + * @return {OptionPage} The topmost visible page. + */ + OptionsPage.getTopmostVisiblePage = function() { + // Check overlays first since they're top-most if visible. + return this.getVisibleOverlay_() || this.getTopmostVisibleNonOverlayPage_(); + }; + + /** + * Returns the currently visible bubble, or null if no bubble is visible. + * @return {AutoCloseBubble} The bubble currently being shown. + */ + OptionsPage.getVisibleBubble = function() { + var bubble = OptionsPage.bubble_; + return bubble && !bubble.hidden ? bubble : null; + }; + + /** + * Shows an informational bubble displaying |content| and pointing at the + * |target| element. If |content| has focusable elements, they join the + * current page's tab order as siblings of |domSibling|. + * @param {HTMLDivElement} content The content of the bubble. + * @param {HTMLElement} target The element at which the bubble points. + * @param {HTMLElement} domSibling The element after which the bubble is added + * to the DOM. + * @param {cr.ui.ArrowLocation} location The arrow location. + */ + OptionsPage.showBubble = function(content, target, domSibling, location) { + OptionsPage.hideBubble(); + + var bubble = new cr.ui.AutoCloseBubble; + bubble.anchorNode = target; + bubble.domSibling = domSibling; + bubble.arrowLocation = location; + bubble.content = content; + bubble.show(); + OptionsPage.bubble_ = bubble; + }; + + /** + * Hides the currently visible bubble, if any. + */ + OptionsPage.hideBubble = function() { + if (OptionsPage.bubble_) + OptionsPage.bubble_.hide(); + }; + + /** + * Shows the tab contents for the given navigation tab. + * @param {!Element} tab The tab that the user clicked. + */ + OptionsPage.showTab = function(tab) { + // Search parents until we find a tab, or the nav bar itself. This allows + // tabs to have child nodes, e.g. labels in separately-styled spans. + while (tab && !tab.classList.contains('subpages-nav-tabs') && + !tab.classList.contains('tab')) { + tab = tab.parentNode; + } + if (!tab || !tab.classList.contains('tab')) + return; + + // Find tab bar of the tab. + var tabBar = tab; + while (tabBar && !tabBar.classList.contains('subpages-nav-tabs')) { + tabBar = tabBar.parentNode; + } + if (!tabBar) + return; + + if (tabBar.activeNavTab != null) { + tabBar.activeNavTab.classList.remove('active-tab'); + $(tabBar.activeNavTab.getAttribute('tab-contents')).classList. + remove('active-tab-contents'); + } + + tab.classList.add('active-tab'); + $(tab.getAttribute('tab-contents')).classList.add('active-tab-contents'); + tabBar.activeNavTab = tab; + }; + + /** + * Registers new options page. + * @param {OptionsPage} page Page to register. + */ + OptionsPage.register = function(page) { + this.registeredPages[page.name.toLowerCase()] = page; + page.initializePage(); + }; + + /** + * Find an enclosing section for an element if it exists. + * @param {Element} element Element to search. + * @return {OptionPage} The section element, or null. + * @private + */ + OptionsPage.findSectionForNode_ = function(node) { + while (node = node.parentNode) { + if (node.nodeName == 'SECTION') + return node; + } + return null; + }; + + /** + * Registers a new Overlay page. + * @param {OptionsPage} overlay Overlay to register. + * @param {OptionsPage} parentPage Associated parent page for this overlay. + * @param {Array} associatedControls Array of control elements associated with + * this page. + */ + OptionsPage.registerOverlay = function(overlay, + parentPage, + associatedControls) { + this.registeredOverlayPages[overlay.name.toLowerCase()] = overlay; + overlay.parentPage = parentPage; + if (associatedControls) { + overlay.associatedControls = associatedControls; + if (associatedControls.length) { + overlay.associatedSection = + this.findSectionForNode_(associatedControls[0]); + } + + // Sanity check. + for (var i = 0; i < associatedControls.length; ++i) { + assert(associatedControls[i], 'Invalid element passed.'); + } + } + + // Reverse the button strip for views. See the documentation of + // reverseButtonStripIfNecessary_() for an explanation of why this is done. + if (cr.isViews) + this.reverseButtonStripIfNecessary_(overlay); + + overlay.tab = undefined; + overlay.isOverlay = true; + overlay.initializePage(); + }; + + /** + * Reverses the child elements of a button strip if it hasn't already been + * reversed. This is necessary because WebKit does not alter the tab order for + * elements that are visually reversed using -webkit-box-direction: reverse, + * and the button order is reversed for views. See http://webk.it/62664 for + * more information. + * @param {Object} overlay The overlay containing the button strip to reverse. + * @private + */ + OptionsPage.reverseButtonStripIfNecessary_ = function(overlay) { + var buttonStrips = + overlay.pageDiv.querySelectorAll('.button-strip:not([reversed])'); + + // Reverse all button-strips in the overlay. + for (var j = 0; j < buttonStrips.length; j++) { + var buttonStrip = buttonStrips[j]; + + var childNodes = buttonStrip.childNodes; + for (var i = childNodes.length - 1; i >= 0; i--) + buttonStrip.appendChild(childNodes[i]); + + buttonStrip.setAttribute('reversed', ''); + } + }; + + /** + * Callback for window.onpopstate to handle back/forward navigations. + * @param {Object} data State data pushed into history. + */ + OptionsPage.setState = function(data) { + if (data && data.pageName) { + var currentOverlay = this.getVisibleOverlay_(); + var lowercaseName = data.pageName.toLowerCase(); + var newPage = this.registeredPages[lowercaseName] || + this.registeredOverlayPages[lowercaseName] || + this.getDefaultPage(); + if (currentOverlay && !currentOverlay.isAncestorOfPage(newPage)) { + currentOverlay.visible = false; + if (currentOverlay.didClosePage) currentOverlay.didClosePage(); + } + this.showPageByName(data.pageName, false); + } + }; + + /** + * Callback for window.onbeforeunload. Used to notify overlays that they will + * be closed. + */ + OptionsPage.willClose = function() { + var overlay = this.getVisibleOverlay_(); + if (overlay && overlay.didClosePage) + overlay.didClosePage(); + }; + + /** + * Freezes/unfreezes the scroll position of the root page container. + * @param {boolean} freeze Whether the page should be frozen. + * @private + */ + OptionsPage.setRootPageFrozen_ = function(freeze) { + var container = $('page-container'); + if (container.classList.contains('frozen') == freeze) + return; + + if (freeze) { + // Lock the width, since auto width computation may change. + container.style.width = window.getComputedStyle(container).width; + container.oldScrollTop = scrollTopForDocument(document); + container.classList.add('frozen'); + var verticalPosition = + container.getBoundingClientRect().top - container.oldScrollTop; + container.style.top = verticalPosition + 'px'; + this.updateFrozenElementHorizontalPosition_(container); + } else { + container.classList.remove('frozen'); + container.style.top = ''; + container.style.left = ''; + container.style.right = ''; + container.style.width = ''; + } + }; + + /** + * Freezes/unfreezes the scroll position of the root page based on the current + * page stack. + */ + OptionsPage.updateRootPageFreezeState = function() { + var topPage = OptionsPage.getTopmostVisiblePage(); + if (topPage) + this.setRootPageFrozen_(topPage.isOverlay); + }; + + /** + * Initializes the complete options page. This will cause all C++ handlers to + * be invoked to do final setup. + */ + OptionsPage.initialize = function() { + chrome.send('coreOptionsInitialize'); + uber.onContentFrameLoaded(); + FocusOutlineManager.forDocument(document); + document.addEventListener('scroll', this.handleScroll_.bind(this)); + + // Trigger the scroll handler manually to set the initial state. + this.handleScroll_(); + + // Shake the dialog if the user clicks outside the dialog bounds. + var containers = [$('overlay-container-1'), $('overlay-container-2')]; + for (var i = 0; i < containers.length; i++) { + var overlay = containers[i]; + cr.ui.overlay.setupOverlay(overlay); + overlay.addEventListener('cancelOverlay', + OptionsPage.cancelOverlay.bind(OptionsPage)); + } + + cr.ui.overlay.globalInitialization(); + }; + + /** + * Does a bounds check for the element on the given x, y client coordinates. + * @param {Element} e The DOM element. + * @param {number} x The client X to check. + * @param {number} y The client Y to check. + * @return {boolean} True if the point falls within the element's bounds. + * @private + */ + OptionsPage.elementContainsPoint_ = function(e, x, y) { + var clientRect = e.getBoundingClientRect(); + return x >= clientRect.left && x <= clientRect.right && + y >= clientRect.top && y <= clientRect.bottom; + }; + + /** + * Called when the page is scrolled; moves elements that are position:fixed + * but should only behave as if they are fixed for vertical scrolling. + * @private + */ + OptionsPage.handleScroll_ = function() { + this.updateAllFrozenElementPositions_(); + }; + + /** + * Updates all frozen pages to match the horizontal scroll position. + * @private + */ + OptionsPage.updateAllFrozenElementPositions_ = function() { + var frozenElements = document.querySelectorAll('.frozen'); + for (var i = 0; i < frozenElements.length; i++) + this.updateFrozenElementHorizontalPosition_(frozenElements[i]); + }; + + /** + * Updates the given frozen element to match the horizontal scroll position. + * @param {HTMLElement} e The frozen element to update. + * @private + */ + OptionsPage.updateFrozenElementHorizontalPosition_ = function(e) { + if (isRTL()) { + e.style.right = OptionsPage.horizontalOffset + 'px'; + } else { + var scrollLeft = scrollLeftForDocument(document); + e.style.left = OptionsPage.horizontalOffset - scrollLeft + 'px'; + } + }; + + /** + * Change the horizontal offset used to reposition elements while showing an + * overlay from the default. + */ + OptionsPage.setHorizontalOffset = function(value) { + OptionsPage.horizontalOffset = value; + }; + + OptionsPage.setClearPluginLSODataEnabled = function(enabled) { + if (enabled) { + document.documentElement.setAttribute( + 'flashPluginSupportsClearSiteData', ''); + } else { + document.documentElement.removeAttribute( + 'flashPluginSupportsClearSiteData'); + } + if (navigator.plugins['Shockwave Flash']) + document.documentElement.setAttribute('hasFlashPlugin', ''); + }; + + OptionsPage.setPepperFlashSettingsEnabled = function(enabled) { + if (enabled) { + document.documentElement.setAttribute( + 'enablePepperFlashSettings', ''); + } else { + document.documentElement.removeAttribute( + 'enablePepperFlashSettings'); + } + }; + + OptionsPage.setIsSettingsApp = function() { + document.documentElement.classList.add('settings-app'); + }; + + OptionsPage.isSettingsApp = function() { + return document.documentElement.classList.contains('settings-app'); + }; + + /** + * Whether the page is still loading (i.e. onload hasn't finished running). + * @return {boolean} Whether the page is still loading. + */ + OptionsPage.isLoading = function() { + return document.documentElement.classList.contains('loading'); + }; + + OptionsPage.prototype = { + __proto__: cr.EventTarget.prototype, + + /** + * The parent page of this option page, or null for top-level pages. + * @type {OptionsPage} + */ + parentPage: null, + + /** + * The section on the parent page that is associated with this page. + * Can be null. + * @type {Element} + */ + associatedSection: null, + + /** + * An array of controls that are associated with this page. The first + * control should be located on a top-level page. + * @type {OptionsPage} + */ + associatedControls: null, + + /** + * Initializes page content. + */ + initializePage: function() {}, + + /** + * Sets focus on the first focusable element. Override for a custom focus + * strategy. + */ + focus: function() { + // Do not change focus if any control on this page is already focused. + if (this.pageDiv.contains(document.activeElement)) + return; + + var elements = this.pageDiv.querySelectorAll( + 'input, list, select, textarea, button'); + for (var i = 0; i < elements.length; i++) { + var element = elements[i]; + // Try to focus. If fails, then continue. + element.focus(); + if (document.activeElement == element) + return; + } + }, + + /** + * Gets the container div for this page if it is an overlay. + * @type {HTMLElement} + */ + get container() { + assert(this.isOverlay); + return this.pageDiv.parentNode; + }, + + /** + * Gets page visibility state. + * @type {boolean} + */ + get visible() { + // If this is an overlay dialog it is no longer considered visible while + // the overlay is fading out. See http://crbug.com/118629. + if (this.isOverlay && + this.container.classList.contains('transparent')) { + return false; + } + if (this.pageDiv.hidden) + return false; + return this.pageDiv.page == this; + }, + + /** + * Sets page visibility. + * @type {boolean} + */ + set visible(visible) { + if ((this.visible && visible) || (!this.visible && !visible)) + return; + + // If using an overlay, the visibility of the dialog is toggled at the + // same time as the overlay to show the dialog's out transition. This + // is handled in setOverlayVisible. + if (this.isOverlay) { + this.setOverlayVisible_(visible); + } else { + this.pageDiv.page = this; + this.pageDiv.hidden = !visible; + this.onVisibilityChanged_(); + } + + cr.dispatchPropertyChange(this, 'visible', visible, !visible); + }, + + /** + * Shows or hides an overlay (including any visible dialog). + * @param {boolean} visible Whether the overlay should be visible or not. + * @private + */ + setOverlayVisible_: function(visible) { + assert(this.isOverlay); + var pageDiv = this.pageDiv; + var container = this.container; + + if (visible) + uber.invokeMethodOnParent('beginInterceptingEvents'); + + if (container.hidden != visible) { + if (visible) { + // If the container is set hidden and then immediately set visible + // again, the fadeCompleted_ callback would cause it to be erroneously + // hidden again. Removing the transparent tag avoids that. + container.classList.remove('transparent'); + + // Hide all dialogs in this container since a different one may have + // been previously visible before fading out. + var pages = container.querySelectorAll('.page'); + for (var i = 0; i < pages.length; i++) + pages[i].hidden = true; + // Show the new dialog. + pageDiv.hidden = false; + pageDiv.page = this; + } + return; + } + + var self = this; + var loading = OptionsPage.isLoading(); + if (!loading) { + // TODO(flackr): Use an event delegate to avoid having to subscribe and + // unsubscribe for webkitTransitionEnd events. + container.addEventListener('webkitTransitionEnd', function f(e) { + if (e.target != e.currentTarget || e.propertyName != 'opacity') + return; + container.removeEventListener('webkitTransitionEnd', f); + self.fadeCompleted_(); + }); + } + + if (visible) { + container.hidden = false; + pageDiv.hidden = false; + pageDiv.page = this; + // NOTE: This is a hacky way to force the container to layout which + // will allow us to trigger the webkit transition. + container.scrollTop; + + this.pageDiv.removeAttribute('aria-hidden'); + if (this.parentPage) { + this.parentPage.pageDiv.parentElement.setAttribute('aria-hidden', + true); + } + container.classList.remove('transparent'); + this.onVisibilityChanged_(); + } else { + // Kick change events for text fields. + if (pageDiv.contains(document.activeElement)) + document.activeElement.blur(); + container.classList.add('transparent'); + } + + if (loading) + this.fadeCompleted_(); + }, + + /** + * Called when a container opacity transition finishes. + * @private + */ + fadeCompleted_: function() { + if (this.container.classList.contains('transparent')) { + this.pageDiv.hidden = true; + this.container.hidden = true; + + if (this.parentPage) + this.parentPage.pageDiv.parentElement.removeAttribute('aria-hidden'); + + if (this.nestingLevel == 1) + uber.invokeMethodOnParent('stopInterceptingEvents'); + + this.onVisibilityChanged_(); + } + }, + + /** + * Called when a page is shown or hidden to update the root options page + * based on this page's visibility. + * @private + */ + onVisibilityChanged_: function() { + OptionsPage.updateRootPageFreezeState(); + + if (this.isOverlay && !this.visible) + OptionsPage.updateScrollPosition_(); + }, + + /** + * The nesting level of this page. + * @type {number} The nesting level of this page (0 for top-level page) + */ + get nestingLevel() { + var level = 0; + var parent = this.parentPage; + while (parent) { + level++; + parent = parent.parentPage; + } + return level; + }, + + /** + * Whether the page is considered 'sticky', such that it will + * remain a top-level page even if sub-pages change. + * @type {boolean} True if this page is sticky. + */ + get sticky() { + return false; + }, + + /** + * Checks whether this page is an ancestor of the given page in terms of + * subpage nesting. + * @param {OptionsPage} page The potential descendent of this page. + * @return {boolean} True if |page| is nested under this page. + */ + isAncestorOfPage: function(page) { + var parent = page.parentPage; + while (parent) { + if (parent == this) + return true; + parent = parent.parentPage; + } + return false; + }, + + /** + * Whether it should be possible to show the page. + * @return {boolean} True if the page should be shown. + */ + canShowPage: function() { + return true; + }, + }; + + // Export + return { + OptionsPage: OptionsPage + }; +}); diff --git a/chromium/chrome/browser/resources/options/options_settings_app.css b/chromium/chrome/browser/resources/options/options_settings_app.css new file mode 100644 index 00000000000..e1bd65801d2 --- /dev/null +++ b/chromium/chrome/browser/resources/options/options_settings_app.css @@ -0,0 +1,47 @@ +/* Copyright 2012 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ + +/* Don't use the large margin used for the navigation bar. Settings App uses a + * 20px margin for headings + 18px for sections. + */ +html.settings-app body.uber-frame { + -webkit-margin-start: 38px; +} + +/* There is a tweak in uber_shared.css to improve touch events around the + * navigation bar (which is not used for the settings app) in reaction to a + * webkit bug (wk95204). We need to reset back to the original style. + */ +@media(pointer:coarse) { + html.settings-app body.uber-frame section { + -webkit-padding-start: 18px; + } + html.settings-app body.uber-frame section > h3 { + -webkit-margin-start: -18px; + } +} + +html.settings-app body.uber-frame header { + left: 20px; + min-width: 400px; +} + +html.settings-app #content-settings-page .content-area { + -webkit-margin-start: 18px; +} + +/* Settings App is narrower due to no navigation margin, so the roomy language + * overlay needs to be trimmed down. 25% is taken off the original height and + * the width is trimmed proportional to (half) the reduction due to the removal + * of the left margin. + */ +html.settings-app .language-options-left { + height: 300px; + width: 228px; +} + +html.settings-app .language-options-right { + height: 300px; + width: 288px; +} diff --git a/chromium/chrome/browser/resources/options/options_settings_app.js b/chromium/chrome/browser/resources/options/options_settings_app.js new file mode 100644 index 00000000000..02ab5569aaa --- /dev/null +++ b/chromium/chrome/browser/resources/options/options_settings_app.js @@ -0,0 +1,52 @@ +// Copyright 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +(function() { + if (document.location != 'chrome://settings-frame/options_settings_app.html') + return; + + OptionsPage.setIsSettingsApp(); + + // Override the offset in the options page. + OptionsPage.setHorizontalOffset(38); + + document.addEventListener('DOMContentLoaded', function() { + // Hide everything by default. + var sections = document.querySelectorAll('section'); + for (var i = 0; i < sections.length; i++) + sections[i].hidden = true; + + var whitelistedSections = [ + 'advanced-settings', + 'downloads-section', + 'handlers-section', + 'languages-section', + 'media-galleries-section', + 'network-section', + 'notifications-section', + 'sync-section' + ]; + + for (var i = 0; i < whitelistedSections.length; i++) + $(whitelistedSections[i]).hidden = false; + + // Avoid showing an empty Users section on ash. Note that profiles-section + // is actually a div element, rather than section, so is not hidden after + // the querySelectorAll(), above. + $('sync-users-section').hidden = $('profiles-section').hidden; + + // Hide Import bookmarks and settings button. + $('import-data').hidden = true; + + // Hide create / edit / delete profile buttons. + $('profiles-create').hidden = true; + $('profiles-delete').hidden = true; + $('profiles-manage').hidden = true; + + // Remove the 'X'es on profiles in the profile list. + $('profiles-list').canDeleteItems = false; + }); + + loadTimeData.overrideValues(loadTimeData.getValue('settingsApp')); +}()); diff --git a/chromium/chrome/browser/resources/options/password_manager.css b/chromium/chrome/browser/resources/options/password_manager.css new file mode 100644 index 00000000000..18314ef98e8 --- /dev/null +++ b/chromium/chrome/browser/resources/options/password_manager.css @@ -0,0 +1,28 @@ +/* Copyright (c) 2012 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#password-manager > div.content-area { + width: 600px; +} + +#password-search-column { + bottom: 10px; + position: absolute; + right: 0; +} + +html[dir=rtl] #password-search-column { + left: 0; + right: auto; +} + +#password-list-headers { + position: relative; + width: 100%; +} + +#passwords-title { + display: inline-block; +} diff --git a/chromium/chrome/browser/resources/options/password_manager.html b/chromium/chrome/browser/resources/options/password_manager.html new file mode 100644 index 00000000000..dfecba6cd07 --- /dev/null +++ b/chromium/chrome/browser/resources/options/password_manager.html @@ -0,0 +1,35 @@ +<div id="password-manager" class="page" hidden> + <div class="close-button"></div> + <h1 i18n-content="passwordsPage"></h1> + <div class="content-area"> + <div id="password-list-headers"> + <div id="passwords-title"> + <h3 i18n-content="savedPasswordsTitle"></h3> + </div> + <div id="password-search-column"> + <input id="password-search-box" type="search" + i18n-values="placeholder:passwordSearchPlaceholder" incremental> + </div> + </div> + <list id="saved-passwords-list" class="settings-list"></list> + <div id="saved-passwords-list-empty-placeholder" + class="settings-list-empty" hidden> + <span i18n-content="passwordsNoPasswordsDescription"></span> + <a target="_blank" i18n-content="learnMore" + i18n-values="href:passwordManagerLearnMoreURL"></a> + </div> + <h3 i18n-content="passwordExceptionsTitle"></h3> + <list id="password-exceptions-list" class="settings-list"></list> + <div id="password-exceptions-list-empty-placeholder" hidden + class="settings-list-empty"> + <span i18n-content="passwordsNoExceptionsDescription"></span> + <a target="_blank" i18n-content="learnMore" + i18n-values="href:passwordManagerLearnMoreURL"></a> + </div> + </div> + <div class="action-area"> + <div class="button-strip"> + <button id="password-manager-confirm" i18n-content="done"></button> + </div> + </div> +</div> diff --git a/chromium/chrome/browser/resources/options/password_manager.js b/chromium/chrome/browser/resources/options/password_manager.js new file mode 100644 index 00000000000..36c04edc604 --- /dev/null +++ b/chromium/chrome/browser/resources/options/password_manager.js @@ -0,0 +1,250 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + /** @const */ var OptionsPage = options.OptionsPage; + /** @const */ var ArrayDataModel = cr.ui.ArrayDataModel; + + ///////////////////////////////////////////////////////////////////////////// + // PasswordManager class: + + /** + * Encapsulated handling of password and exceptions page. + * @constructor + */ + function PasswordManager() { + this.activeNavTab = null; + OptionsPage.call(this, + 'passwords', + loadTimeData.getString('passwordsPageTabTitle'), + 'password-manager'); + } + + cr.addSingletonGetter(PasswordManager); + + PasswordManager.prototype = { + __proto__: OptionsPage.prototype, + + /** + * The saved passwords list. + * @type {DeletableItemList} + * @private + */ + savedPasswordsList_: null, + + /** + * The password exceptions list. + * @type {DeletableItemList} + * @private + */ + passwordExceptionsList_: null, + + /** + * The timer id of the timer set on search query change events. + * @type {number} + * @private + */ + queryDelayTimerId_: 0, + + /** + * The most recent search query, or null if the query is empty. + * @type {?string} + * @private + */ + lastQuery_: null, + + /** @override */ + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + + $('password-manager-confirm').onclick = function() { + OptionsPage.closeOverlay(); + }; + + $('password-search-box').addEventListener('search', + this.handleSearchQueryChange_.bind(this)); + + this.createSavedPasswordsList_(); + this.createPasswordExceptionsList_(); + }, + + /** @override */ + canShowPage: function() { + return !(cr.isChromeOS && UIAccountTweaks.loggedInAsGuest()); + }, + + /** @override */ + didShowPage: function() { + // Updating the password lists may cause a blocking platform dialog pop up + // (Mac, Linux), so we delay this operation until the page is shown. + chrome.send('updatePasswordLists'); + $('password-search-box').focus(); + }, + + /** + * Creates, decorates and initializes the saved passwords list. + * @private + */ + createSavedPasswordsList_: function() { + this.savedPasswordsList_ = $('saved-passwords-list'); + options.passwordManager.PasswordsList.decorate(this.savedPasswordsList_); + this.savedPasswordsList_.autoExpands = true; + }, + + /** + * Creates, decorates and initializes the password exceptions list. + * @private + */ + createPasswordExceptionsList_: function() { + this.passwordExceptionsList_ = $('password-exceptions-list'); + options.passwordManager.PasswordExceptionsList.decorate( + this.passwordExceptionsList_); + this.passwordExceptionsList_.autoExpands = true; + }, + + /** + * Handles search query changes. + * @param {!Event} e The event object. + * @private + */ + handleSearchQueryChange_: function(e) { + if (this.queryDelayTimerId_) + window.clearTimeout(this.queryDelayTimerId_); + + // Searching cookies uses a timeout of 500ms. We use a shorter timeout + // because there are probably fewer passwords and we want the UI to be + // snappier since users will expect that it's "less work." + this.queryDelayTimerId_ = window.setTimeout( + this.searchPasswords_.bind(this), 250); + }, + + /** + * Search passwords using text in |password-search-box|. + * @private + */ + searchPasswords_: function() { + this.queryDelayTimerId_ = 0; + var filter = $('password-search-box').value; + filter = (filter == '') ? null : filter; + if (this.lastQuery_ != filter) { + this.lastQuery_ = filter; + // Searching for passwords has the side effect of requerying the + // underlying password store. This is done intentionally, as on OS X and + // Linux they can change from outside and we won't be notified of it. + chrome.send('updatePasswordLists'); + } + }, + + /** + * Updates the visibility of the list and empty list placeholder. + * @param {!List} list The list to toggle visilibility for. + */ + updateListVisibility_: function(list) { + var empty = list.dataModel.length == 0; + var listPlaceHolderID = list.id + '-empty-placeholder'; + list.hidden = empty; + $(listPlaceHolderID).hidden = !empty; + }, + + /** + * Updates the data model for the saved passwords list with the values from + * |entries|. + * @param {Array} entries The list of saved password data. + */ + setSavedPasswordsList_: function(entries) { + if (this.lastQuery_) { + // Implement password searching here in javascript, rather than in C++. + // The number of saved passwords shouldn't be too big for us to handle. + var query = this.lastQuery_; + var filter = function(entry, index, list) { + // Search both URL and username. + if (entry[0].toLowerCase().indexOf(query.toLowerCase()) >= 0 || + entry[1].toLowerCase().indexOf(query.toLowerCase()) >= 0) { + // Keep the original index so we can delete correctly. See also + // deleteItemAtIndex() in password_manager_list.js that uses this. + entry[3] = index; + return true; + } + return false; + }; + entries = entries.filter(filter); + } + this.savedPasswordsList_.dataModel = new ArrayDataModel(entries); + this.updateListVisibility_(this.savedPasswordsList_); + }, + + /** + * Updates the data model for the password exceptions list with the values + * from |entries|. + * @param {Array} entries The list of password exception data. + */ + setPasswordExceptionsList_: function(entries) { + this.passwordExceptionsList_.dataModel = new ArrayDataModel(entries); + this.updateListVisibility_(this.passwordExceptionsList_); + }, + + /** + * Reveals the password for a saved password entry. This is called by the + * backend after it has authenticated the user. + * @param {number} index The original index of the entry in the model. + * @param {string} password The saved password. + */ + showPassword_: function(index, password) { + var model = this.savedPasswordsList_.dataModel; + if (this.lastQuery_) { + // When a filter is active, |index| does not represent the current + // index in the model, but each entry stores its original index, so + // we can find the item using a linear search. + for (var i = 0; i < model.length; ++i) { + if (model.item(i)[3] == index) { + index = i; + break; + } + } + } + + // Reveal the password in the UI. + var item = this.savedPasswordsList_.getListItemByIndex(index); + item.showPassword(password); + }, + }; + + /** + * Removes a saved password. + * @param {number} rowIndex indicating the row to remove. + */ + PasswordManager.removeSavedPassword = function(rowIndex) { + chrome.send('removeSavedPassword', [String(rowIndex)]); + }; + + /** + * Removes a password exception. + * @param {number} rowIndex indicating the row to remove. + */ + PasswordManager.removePasswordException = function(rowIndex) { + chrome.send('removePasswordException', [String(rowIndex)]); + }; + + PasswordManager.requestShowPassword = function(index) { + chrome.send('requestShowPassword', [index]); + }; + + // Forward public APIs to private implementations on the singleton instance. + [ + 'setSavedPasswordsList', + 'setPasswordExceptionsList', + 'showPassword' + ].forEach(function(name) { + PasswordManager[name] = function() { + var instance = PasswordManager.getInstance(); + return instance[name + '_'].apply(instance, arguments); + }; + }); + + // Export + return { + PasswordManager: PasswordManager + }; + +}); diff --git a/chromium/chrome/browser/resources/options/password_manager_list.css b/chromium/chrome/browser/resources/options/password_manager_list.css new file mode 100644 index 00000000000..8688619c287 --- /dev/null +++ b/chromium/chrome/browser/resources/options/password_manager_list.css @@ -0,0 +1,58 @@ +/* Copyright (c) 2012 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ + +#saved-passwords-list .list-inline-button { + -webkit-transition: opacity 150ms; + background: rgb(138, 170, 237); + font-size: 0.9em; + height: 18px; + padding: 0 2px; + position: absolute; + top: 3px; +} + +html[dir='ltr'] #saved-passwords-list .list-inline-button { + right: 2px; +} + +html[dir='rtl'] #saved-passwords-list .list-inline-button { + left: 2px; +} + +input[type='password'].inactive-password { + background: transparent; + border: none; +} + +#saved-passwords-list .url { + box-sizing: border-box; + width: 40%; +} + +#saved-passwords-list .name { + -webkit-box-flex: 1; + width: 20%; +} + +#saved-passwords-list .password { + -webkit-box-flex: 1; + position: relative; +} + +#saved-passwords-list .password input[type='password'], +#saved-passwords-list .password input[type='text'] { + box-sizing: border-box; + width: 100%; +} + +#password-exceptions-list .url { + -webkit-box-flex: 1; +} + +#saved-passwords-list .url, +#saved-passwords-list .name, +#password-exceptions-list .url { + overflow: hidden; + text-overflow: ellipsis; +} diff --git a/chromium/chrome/browser/resources/options/password_manager_list.js b/chromium/chrome/browser/resources/options/password_manager_list.js new file mode 100644 index 00000000000..9442dee28a5 --- /dev/null +++ b/chromium/chrome/browser/resources/options/password_manager_list.js @@ -0,0 +1,342 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options.passwordManager', function() { + /** @const */ var ArrayDataModel = cr.ui.ArrayDataModel; + /** @const */ var DeletableItemList = options.DeletableItemList; + /** @const */ var DeletableItem = options.DeletableItem; + /** @const */ var List = cr.ui.List; + + /** + * Creates a new passwords list item. + * @param {ArrayDataModel} dataModel The data model that contains this item. + * @param {Array} entry An array of the form [url, username, password]. When + * the list has been filtered, a fourth element [index] may be present. + * @param {boolean} showPasswords If true, add a button to the element to + * allow the user to reveal the saved password. + * @constructor + * @extends {cr.ui.ListItem} + */ + function PasswordListItem(dataModel, entry, showPasswords) { + var el = cr.doc.createElement('div'); + el.dataItem = entry; + el.dataModel = dataModel; + el.__proto__ = PasswordListItem.prototype; + el.decorate(showPasswords); + + return el; + } + + PasswordListItem.prototype = { + __proto__: DeletableItem.prototype, + + /** @override */ + decorate: function(showPasswords) { + DeletableItem.prototype.decorate.call(this); + + // The URL of the site. + var urlLabel = this.ownerDocument.createElement('div'); + urlLabel.classList.add('favicon-cell'); + urlLabel.classList.add('weakrtl'); + urlLabel.classList.add('url'); + urlLabel.setAttribute('title', this.url); + urlLabel.textContent = this.url; + + // The favicon URL is prefixed with "origin/", which essentially removes + // the URL path past the top-level domain and ensures that a scheme (e.g., + // http) is being used. This ensures that the favicon returned is the + // default favicon for the domain and that the URL has a scheme if none + // is present in the password manager. + urlLabel.style.backgroundImage = getFaviconImageSet( + 'origin/' + this.url, 16); + this.contentElement.appendChild(urlLabel); + + // The stored username. + var usernameLabel = this.ownerDocument.createElement('div'); + usernameLabel.className = 'name'; + usernameLabel.textContent = this.username; + this.contentElement.appendChild(usernameLabel); + + // The stored password. + var passwordInputDiv = this.ownerDocument.createElement('div'); + passwordInputDiv.className = 'password'; + + // The password input field. + var passwordInput = this.ownerDocument.createElement('input'); + passwordInput.type = 'password'; + passwordInput.className = 'inactive-password'; + passwordInput.readOnly = true; + passwordInput.value = showPasswords ? this.password : '********'; + passwordInputDiv.appendChild(passwordInput); + this.passwordField = passwordInput; + + // The show/hide button. + if (showPasswords) { + var button = this.ownerDocument.createElement('button'); + button.hidden = true; + button.className = 'list-inline-button custom-appearance'; + button.textContent = loadTimeData.getString('passwordShowButton'); + button.addEventListener('click', this.onClick_.bind(this), true); + button.addEventListener('mousedown', function(event) { + // Don't focus on this button by mousedown. + event.preventDefault(); + // Don't handle list item selection. It causes focus change. + event.stopPropagation(); + }, false); + passwordInputDiv.appendChild(button); + this.passwordShowButton = button; + } + + this.contentElement.appendChild(passwordInputDiv); + }, + + /** @override */ + selectionChanged: function() { + var input = this.passwordField; + var button = this.passwordShowButton; + // The button doesn't exist when passwords can't be shown. + if (!button) + return; + + if (this.selected) { + input.classList.remove('inactive-password'); + button.hidden = false; + } else { + input.classList.add('inactive-password'); + button.hidden = true; + } + }, + + /** + * Reveals the plain text password of this entry. + */ + showPassword: function(password) { + this.passwordField.value = password; + this.passwordField.type = 'text'; + + var button = this.passwordShowButton; + if (button) + button.textContent = loadTimeData.getString('passwordHideButton'); + }, + + /** + * Hides the plain text password of this entry. + */ + hidePassword: function() { + this.passwordField.type = 'password'; + + var button = this.passwordShowButton; + if (button) + button.textContent = loadTimeData.getString('passwordShowButton'); + }, + + /** + * Get the original index of this item in the data model. + * @return {number} The index. + * @private + */ + getOriginalIndex_: function() { + var index = this.dataItem[3]; + return index ? index : this.dataModel.indexOf(this.dataItem); + }, + + /** + * On-click event handler. Swaps the type of the input field from password + * to text and back. + * @private + */ + onClick_: function(event) { + if (this.passwordField.type == 'password') { + // After the user is authenticated, showPassword() will be called. + PasswordManager.requestShowPassword(this.getOriginalIndex_()); + } else { + this.hidePassword(); + } + }, + + /** + * Get and set the URL for the entry. + * @type {string} + */ + get url() { + return this.dataItem[0]; + }, + set url(url) { + this.dataItem[0] = url; + }, + + /** + * Get and set the username for the entry. + * @type {string} + */ + get username() { + return this.dataItem[1]; + }, + set username(username) { + this.dataItem[1] = username; + }, + + /** + * Get and set the password for the entry. + * @type {string} + */ + get password() { + return this.dataItem[2]; + }, + set password(password) { + this.dataItem[2] = password; + }, + }; + + /** + * Creates a new PasswordExceptions list item. + * @param {Array} entry A pair of the form [url, username]. + * @constructor + * @extends {Deletable.ListItem} + */ + function PasswordExceptionsListItem(entry) { + var el = cr.doc.createElement('div'); + el.dataItem = entry; + el.__proto__ = PasswordExceptionsListItem.prototype; + el.decorate(); + + return el; + } + + PasswordExceptionsListItem.prototype = { + __proto__: DeletableItem.prototype, + + /** + * Call when an element is decorated as a list item. + */ + decorate: function() { + DeletableItem.prototype.decorate.call(this); + + // The URL of the site. + var urlLabel = this.ownerDocument.createElement('div'); + urlLabel.className = 'url'; + urlLabel.classList.add('favicon-cell'); + urlLabel.classList.add('weakrtl'); + urlLabel.textContent = this.url; + + // The favicon URL is prefixed with "origin/", which essentially removes + // the URL path past the top-level domain and ensures that a scheme (e.g., + // http) is being used. This ensures that the favicon returned is the + // default favicon for the domain and that the URL has a scheme if none + // is present in the password manager. + urlLabel.style.backgroundImage = getFaviconImageSet( + 'origin/' + this.url, 16); + this.contentElement.appendChild(urlLabel); + }, + + /** + * Get the url for the entry. + * @type {string} + */ + get url() { + return this.dataItem; + }, + set url(url) { + this.dataItem = url; + }, + }; + + /** + * Create a new passwords list. + * @constructor + * @extends {cr.ui.List} + */ + var PasswordsList = cr.ui.define('list'); + + PasswordsList.prototype = { + __proto__: DeletableItemList.prototype, + + /** + * Whether passwords can be revealed or not. + * @type {boolean} + * @private + */ + showPasswords_: true, + + /** @override */ + decorate: function() { + DeletableItemList.prototype.decorate.call(this); + Preferences.getInstance().addEventListener( + 'profile.password_manager_allow_show_passwords', + this.onPreferenceChanged_.bind(this)); + }, + + /** + * Listener for changes on the preference. + * @param {Event} event The preference update event. + * @private + */ + onPreferenceChanged_: function(event) { + this.showPasswords_ = event.value.value; + this.redraw(); + }, + + /** @override */ + createItem: function(entry) { + var showPasswords = this.showPasswords_; + + if (loadTimeData.getBoolean('disableShowPasswords')) + showPasswords = false; + + return new PasswordListItem(this.dataModel, entry, showPasswords); + }, + + /** @override */ + deleteItemAtIndex: function(index) { + var item = this.dataModel.item(index); + if (item && item.length > 3) { + // The fourth element, if present, is the original index to delete. + index = item[3]; + } + PasswordManager.removeSavedPassword(index); + }, + + /** + * The length of the list. + */ + get length() { + return this.dataModel.length; + }, + }; + + /** + * Create a new passwords list. + * @constructor + * @extends {cr.ui.List} + */ + var PasswordExceptionsList = cr.ui.define('list'); + + PasswordExceptionsList.prototype = { + __proto__: DeletableItemList.prototype, + + /** @override */ + createItem: function(entry) { + return new PasswordExceptionsListItem(entry); + }, + + /** @override */ + deleteItemAtIndex: function(index) { + PasswordManager.removePasswordException(index); + }, + + /** + * The length of the list. + */ + get length() { + return this.dataModel.length; + }, + }; + + return { + PasswordListItem: PasswordListItem, + PasswordExceptionsListItem: PasswordExceptionsListItem, + PasswordsList: PasswordsList, + PasswordExceptionsList: PasswordExceptionsList, + }; +}); diff --git a/chromium/chrome/browser/resources/options/pref_ui.js b/chromium/chrome/browser/resources/options/pref_ui.js new file mode 100644 index 00000000000..0775306d776 --- /dev/null +++ b/chromium/chrome/browser/resources/options/pref_ui.js @@ -0,0 +1,557 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + + var Preferences = options.Preferences; + + /** + * Allows an element to be disabled for several reasons. + * The element is disabled if at least one reason is true, and the reasons + * can be set separately. + * @private + * @param {!HTMLElement} el The element to update. + * @param {string} reason The reason for disabling the element. + * @param {boolean} disabled Whether the element should be disabled or enabled + * for the given |reason|. + */ + function updateDisabledState_(el, reason, disabled) { + if (!el.disabledReasons) + el.disabledReasons = {}; + if (el.disabled && (Object.keys(el.disabledReasons).length == 0)) { + // The element has been previously disabled without a reason, so we add + // one to keep it disabled. + el.disabledReasons.other = true; + } + if (!el.disabled) { + // If the element is not disabled, there should be no reason, except for + // 'other'. + delete el.disabledReasons.other; + if (Object.keys(el.disabledReasons).length > 0) + console.error('Element is not disabled but should be'); + } + if (disabled) { + el.disabledReasons[reason] = true; + } else { + delete el.disabledReasons[reason]; + } + el.disabled = Object.keys(el.disabledReasons).length > 0; + } + + ///////////////////////////////////////////////////////////////////////////// + // PrefInputElement class: + + // Define a constructor that uses an input element as its underlying element. + var PrefInputElement = cr.ui.define('input'); + + PrefInputElement.prototype = { + // Set up the prototype chain + __proto__: HTMLInputElement.prototype, + + /** + * Initialization function for the cr.ui framework. + */ + decorate: function() { + var self = this; + + // Listen for user events. + this.addEventListener('change', this.handleChange_.bind(this)); + + // Listen for pref changes. + Preferences.getInstance().addEventListener(this.pref, function(event) { + if (event.value.uncommitted && !self.dialogPref) + return; + self.updateStateFromPref_(event); + updateDisabledState_(self, 'notUserModifiable', event.value.disabled); + self.controlledBy = event.value.controlledBy; + }); + }, + + /** + * Handle changes to the input element's state made by the user. If a custom + * change handler does not suppress it, a default handler is invoked that + * updates the associated pref. + * @param {Event} event Change event. + * @private + */ + handleChange_: function(event) { + if (!this.customChangeHandler(event)) + this.updatePrefFromState_(); + }, + + /** + * Update the input element's state when the associated pref changes. + * @param {Event} event Pref change event. + * @private + */ + updateStateFromPref_: function(event) { + this.value = event.value.value; + }, + + /** + * See |updateDisabledState_| above. + */ + setDisabled: function(reason, disabled) { + updateDisabledState_(this, reason, disabled); + }, + + /** + * Custom change handler that is invoked first when the user makes changes + * to the input element's state. If it returns false, a default handler is + * invoked next that updates the associated pref. If it returns true, the + * default handler is suppressed (i.e., this works like stopPropagation or + * cancelBubble). + * @param {Event} event Input element change event. + */ + customChangeHandler: function(event) { + return false; + }, + }; + + /** + * The name of the associated preference. + * @type {string} + */ + cr.defineProperty(PrefInputElement, 'pref', cr.PropertyKind.ATTR); + + /** + * The data type of the associated preference, only relevant for derived + * classes that support different data types. + * @type {string} + */ + cr.defineProperty(PrefInputElement, 'dataType', cr.PropertyKind.ATTR); + + /** + * Whether this input element is part of a dialog. If so, changes take effect + * in the settings UI immediately but are only actually committed when the + * user confirms the dialog. If the user cancels the dialog instead, the + * changes are rolled back in the settings UI and never committed. + * @type {boolean} + */ + cr.defineProperty(PrefInputElement, 'dialogPref', cr.PropertyKind.BOOL_ATTR); + + /** + * Whether the associated preference is controlled by a source other than the + * user's setting (can be 'policy', 'extension', 'recommended' or unset). + * @type {string} + */ + cr.defineProperty(PrefInputElement, 'controlledBy', cr.PropertyKind.ATTR); + + /** + * The user metric string. + * @type {string} + */ + cr.defineProperty(PrefInputElement, 'metric', cr.PropertyKind.ATTR); + + ///////////////////////////////////////////////////////////////////////////// + // PrefCheckbox class: + + // Define a constructor that uses an input element as its underlying element. + var PrefCheckbox = cr.ui.define('input'); + + PrefCheckbox.prototype = { + // Set up the prototype chain + __proto__: PrefInputElement.prototype, + + /** + * Initialization function for the cr.ui framework. + */ + decorate: function() { + PrefInputElement.prototype.decorate.call(this); + this.type = 'checkbox'; + }, + + /** + * Update the associated pref when when the user makes changes to the + * checkbox state. + * @private + */ + updatePrefFromState_: function() { + var value = this.inverted_pref ? !this.checked : this.checked; + Preferences.setBooleanPref(this.pref, value, + !this.dialogPref, this.metric); + }, + + /** + * Update the checkbox state when the associated pref changes. + * @param {Event} event Pref change event. + * @private + */ + updateStateFromPref_: function(event) { + var value = Boolean(event.value.value); + this.checked = this.inverted_pref ? !value : value; + }, + }; + + /** + * Whether the mapping between checkbox state and associated pref is inverted. + * @type {boolean} + */ + cr.defineProperty(PrefCheckbox, 'inverted_pref', cr.PropertyKind.BOOL_ATTR); + + ///////////////////////////////////////////////////////////////////////////// + // PrefNumber class: + + // Define a constructor that uses an input element as its underlying element. + var PrefNumber = cr.ui.define('input'); + + PrefNumber.prototype = { + // Set up the prototype chain + __proto__: PrefInputElement.prototype, + + /** + * Initialization function for the cr.ui framework. + */ + decorate: function() { + PrefInputElement.prototype.decorate.call(this); + this.type = 'number'; + }, + + /** + * Update the associated pref when when the user inputs a number. + * @private + */ + updatePrefFromState_: function() { + if (this.validity.valid) { + Preferences.setIntegerPref(this.pref, this.value, + !this.dialogPref, this.metric); + } + }, + }; + + ///////////////////////////////////////////////////////////////////////////// + // PrefRadio class: + + //Define a constructor that uses an input element as its underlying element. + var PrefRadio = cr.ui.define('input'); + + PrefRadio.prototype = { + // Set up the prototype chain + __proto__: PrefInputElement.prototype, + + /** + * Initialization function for the cr.ui framework. + */ + decorate: function() { + PrefInputElement.prototype.decorate.call(this); + this.type = 'radio'; + }, + + /** + * Update the associated pref when when the user selects the radio button. + * @private + */ + updatePrefFromState_: function() { + if (this.value == 'true' || this.value == 'false') { + Preferences.setBooleanPref(this.pref, + this.value == String(this.checked), + !this.dialogPref, this.metric); + } else { + Preferences.setIntegerPref(this.pref, this.value, + !this.dialogPref, this.metric); + } + }, + + /** + * Update the radio button state when the associated pref changes. + * @param {Event} event Pref change event. + * @private + */ + updateStateFromPref_: function(event) { + this.checked = this.value == String(event.value.value); + }, + }; + + ///////////////////////////////////////////////////////////////////////////// + // PrefRange class: + + // Define a constructor that uses an input element as its underlying element. + var PrefRange = cr.ui.define('input'); + + PrefRange.prototype = { + // Set up the prototype chain + __proto__: PrefInputElement.prototype, + + /** + * The map from slider position to corresponding pref value. + */ + valueMap: undefined, + + /** + * Initialization function for the cr.ui framework. + */ + decorate: function() { + PrefInputElement.prototype.decorate.call(this); + this.type = 'range'; + + // Listen for user events. + // TODO(jhawkins): Add onmousewheel handling once the associated WK bug is + // fixed. + // https://bugs.webkit.org/show_bug.cgi?id=52256 + this.addEventListener('keyup', this.handleRelease_.bind(this)); + this.addEventListener('mouseup', this.handleRelease_.bind(this)); + }, + + /** + * Update the associated pref when when the user releases the slider. + * @private + */ + updatePrefFromState_: function() { + Preferences.setIntegerPref(this.pref, this.mapPositionToPref(this.value), + !this.dialogPref, this.metric); + }, + + /** + * Ignore changes to the slider position made by the user while the slider + * has not been released. + * @private + */ + handleChange_: function() { + }, + + /** + * Handle changes to the slider position made by the user when the slider is + * released. If a custom change handler does not suppress it, a default + * handler is invoked that updates the associated pref. + * @param {Event} event Change event. + * @private + */ + handleRelease_: function(event) { + if (!this.customChangeHandler(event)) + this.updatePrefFromState_(); + }, + + /** + * Update the slider position when the associated pref changes. + * @param {Event} event Pref change event. + * @private + */ + updateStateFromPref_: function(event) { + var value = event.value.value; + this.value = this.valueMap ? this.valueMap.indexOf(value) : value; + }, + + /** + * Map slider position to the range of values provided by the client, + * represented by |valueMap|. + * @param {number} position The slider position to map. + */ + mapPositionToPref: function(position) { + return this.valueMap ? this.valueMap[position] : position; + }, + }; + + ///////////////////////////////////////////////////////////////////////////// + // PrefSelect class: + + // Define a constructor that uses a select element as its underlying element. + var PrefSelect = cr.ui.define('select'); + + PrefSelect.prototype = { + // Set up the prototype chain + __proto__: PrefInputElement.prototype, + + /** + * Update the associated pref when when the user selects an item. + * @private + */ + updatePrefFromState_: function() { + var value = this.options[this.selectedIndex].value; + switch (this.dataType) { + case 'number': + Preferences.setIntegerPref(this.pref, value, + !this.dialogPref, this.metric); + break; + case 'double': + Preferences.setDoublePref(this.pref, value, + !this.dialogPref, this.metric); + break; + case 'boolean': + Preferences.setBooleanPref(this.pref, value == 'true', + !this.dialogPref, this.metric); + break; + case 'string': + Preferences.setStringPref(this.pref, value, + !this.dialogPref, this.metric); + break; + default: + console.error('Unknown data type for <select> UI element: ' + + this.dataType); + } + }, + + /** + * Update the selected item when the associated pref changes. + * @param {Event} event Pref change event. + * @private + */ + updateStateFromPref_: function(event) { + // Make sure the value is a string, because the value is stored as a + // string in the HTMLOptionElement. + value = String(event.value.value); + + var found = false; + for (var i = 0; i < this.options.length; i++) { + if (this.options[i].value == value) { + this.selectedIndex = i; + found = true; + } + } + + // Item not found, select first item. + if (!found) + this.selectedIndex = 0; + + // The "onchange" event automatically fires when the user makes a manual + // change. It should never be fired for a programmatic change. However, + // these two lines were here already and it is hard to tell who may be + // relying on them. + if (this.onchange) + this.onchange(event); + }, + }; + + ///////////////////////////////////////////////////////////////////////////// + // PrefTextField class: + + // Define a constructor that uses an input element as its underlying element. + var PrefTextField = cr.ui.define('input'); + + PrefTextField.prototype = { + // Set up the prototype chain + __proto__: PrefInputElement.prototype, + + /** + * Initialization function for the cr.ui framework. + */ + decorate: function() { + PrefInputElement.prototype.decorate.call(this); + var self = this; + + // Listen for user events. + window.addEventListener('unload', function() { + if (document.activeElement == self) + self.blur(); + }); + }, + + /** + * Update the associated pref when when the user inputs text. + * @private + */ + updatePrefFromState_: function(event) { + switch (this.dataType) { + case 'number': + Preferences.setIntegerPref(this.pref, this.value, + !this.dialogPref, this.metric); + break; + case 'double': + Preferences.setDoublePref(this.pref, this.value, + !this.dialogPref, this.metric); + break; + case 'url': + Preferences.setURLPref(this.pref, this.value, + !this.dialogPref, this.metric); + break; + default: + Preferences.setStringPref(this.pref, this.value, + !this.dialogPref, this.metric); + break; + } + }, + }; + + ///////////////////////////////////////////////////////////////////////////// + // PrefPortNumber class: + + // Define a constructor that uses an input element as its underlying element. + var PrefPortNumber = cr.ui.define('input'); + + PrefPortNumber.prototype = { + // Set up the prototype chain + __proto__: PrefTextField.prototype, + + /** + * Initialization function for the cr.ui framework. + */ + decorate: function() { + var self = this; + self.type = 'text'; + self.dataType = 'number'; + PrefTextField.prototype.decorate.call(this); + self.oninput = function() { + // Note that using <input type="number"> is insufficient to restrict + // the input as it allows negative numbers and does not limit the + // number of charactes typed even if a range is set. Furthermore, + // it sometimes produces strange repaint artifacts. + var filtered = self.value.replace(/[^0-9]/g, ''); + if (filtered != self.value) + self.value = filtered; + }; + } + }; + + ///////////////////////////////////////////////////////////////////////////// + // PrefButton class: + + // Define a constructor that uses a button element as its underlying element. + var PrefButton = cr.ui.define('button'); + + PrefButton.prototype = { + // Set up the prototype chain + __proto__: HTMLButtonElement.prototype, + + /** + * Initialization function for the cr.ui framework. + */ + decorate: function() { + var self = this; + + // Listen for pref changes. + // This element behaves like a normal button and does not affect the + // underlying preference; it just becomes disabled when the preference is + // managed, and its value is false. This is useful for buttons that should + // be disabled when the underlying Boolean preference is set to false by a + // policy or extension. + Preferences.getInstance().addEventListener(this.pref, function(event) { + updateDisabledState_(self, 'notUserModifiable', + event.value.disabled && !event.value.value); + self.controlledBy = event.value.controlledBy; + }); + }, + + /** + * See |updateDisabledState_| above. + */ + setDisabled: function(reason, disabled) { + updateDisabledState_(this, reason, disabled); + }, + }; + + /** + * The name of the associated preference. + * @type {string} + */ + cr.defineProperty(PrefButton, 'pref', cr.PropertyKind.ATTR); + + /** + * Whether the associated preference is controlled by a source other than the + * user's setting (can be 'policy', 'extension', 'recommended' or unset). + * @type {string} + */ + cr.defineProperty(PrefButton, 'controlledBy', cr.PropertyKind.ATTR); + + // Export + return { + PrefCheckbox: PrefCheckbox, + PrefNumber: PrefNumber, + PrefRadio: PrefRadio, + PrefRange: PrefRange, + PrefSelect: PrefSelect, + PrefTextField: PrefTextField, + PrefPortNumber: PrefPortNumber, + PrefButton: PrefButton + }; + +}); diff --git a/chromium/chrome/browser/resources/options/preferences.js b/chromium/chrome/browser/resources/options/preferences.js new file mode 100644 index 00000000000..6f8cc829417 --- /dev/null +++ b/chromium/chrome/browser/resources/options/preferences.js @@ -0,0 +1,338 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + + ///////////////////////////////////////////////////////////////////////////// + // Preferences class: + + /** + * Preferences class manages access to Chrome profile preferences. + * @constructor + */ + function Preferences() { + // Map of registered preferences. + this.registeredPreferences_ = {}; + } + + cr.addSingletonGetter(Preferences); + + /** + * Sets a Boolean preference and signals its new value. + * @param {string} name Preference name. + * @param {boolean} value New preference value. + * @param {boolean} commit Whether to commit the change to Chrome. + * @param {string} metric User metrics identifier. + */ + Preferences.setBooleanPref = function(name, value, commit, metric) { + if (!commit) { + Preferences.getInstance().setPrefNoCommit_(name, 'bool', Boolean(value)); + return; + } + + var argumentList = [name, Boolean(value)]; + if (metric != undefined) argumentList.push(metric); + chrome.send('setBooleanPref', argumentList); + }; + + /** + * Sets an integer preference and signals its new value. + * @param {string} name Preference name. + * @param {number} value New preference value. + * @param {boolean} commit Whether to commit the change to Chrome. + * @param {string} metric User metrics identifier. + */ + Preferences.setIntegerPref = function(name, value, commit, metric) { + if (!commit) { + Preferences.getInstance().setPrefNoCommit_(name, 'int', Number(value)); + return; + } + + var argumentList = [name, Number(value)]; + if (metric != undefined) argumentList.push(metric); + chrome.send('setIntegerPref', argumentList); + }; + + /** + * Sets a double-valued preference and signals its new value. + * @param {string} name Preference name. + * @param {number} value New preference value. + * @param {boolean} commit Whether to commit the change to Chrome. + * @param {string} metric User metrics identifier. + */ + Preferences.setDoublePref = function(name, value, commit, metric) { + if (!commit) { + Preferences.getInstance().setPrefNoCommit_(name, 'double', Number(value)); + return; + } + + var argumentList = [name, Number(value)]; + if (metric != undefined) argumentList.push(metric); + chrome.send('setDoublePref', argumentList); + }; + + /** + * Sets a string preference and signals its new value. + * @param {string} name Preference name. + * @param {string} value New preference value. + * @param {boolean} commit Whether to commit the change to Chrome. + * @param {string} metric User metrics identifier. + */ + Preferences.setStringPref = function(name, value, commit, metric) { + if (!commit) { + Preferences.getInstance().setPrefNoCommit_(name, 'string', String(value)); + return; + } + + var argumentList = [name, String(value)]; + if (metric != undefined) argumentList.push(metric); + chrome.send('setStringPref', argumentList); + }; + + /** + * Sets a string preference that represents a URL and signals its new value. + * The value will be fixed to be a valid URL when it gets committed to Chrome. + * @param {string} name Preference name. + * @param {string} value New preference value. + * @param {boolean} commit Whether to commit the change to Chrome. + * @param {string} metric User metrics identifier. + */ + Preferences.setURLPref = function(name, value, commit, metric) { + if (!commit) { + Preferences.getInstance().setPrefNoCommit_(name, 'url', String(value)); + return; + } + + var argumentList = [name, String(value)]; + if (metric != undefined) argumentList.push(metric); + chrome.send('setURLPref', argumentList); + }; + + /** + * Sets a JSON list preference and signals its new value. + * @param {string} name Preference name. + * @param {Array} value New preference value. + * @param {boolean} commit Whether to commit the change to Chrome. + * @param {string} metric User metrics identifier. + */ + Preferences.setListPref = function(name, value, commit, metric) { + if (!commit) { + Preferences.getInstance().setPrefNoCommit_(name, 'list', value); + return; + } + + var argumentList = [name, JSON.stringify(value)]; + if (metric != undefined) argumentList.push(metric); + chrome.send('setListPref', argumentList); + }; + + /** + * Clears the user setting for a preference and signals its new effective + * value. + * @param {string} name Preference name. + * @param {boolean} commit Whether to commit the change to Chrome. + * @param {string} metric User metrics identifier. + */ + Preferences.clearPref = function(name, commit, metric) { + if (!commit) { + Preferences.getInstance().clearPrefNoCommit_(name); + return; + } + + var argumentList = [name]; + if (metric != undefined) argumentList.push(metric); + chrome.send('clearPref', argumentList); + }; + + Preferences.prototype = { + __proto__: cr.EventTarget.prototype, + + /** + * Adds an event listener to the target. + * @param {string} type The name of the event. + * @param {!Function|{handleEvent:Function}} handler The handler for the + * event. This is called when the event is dispatched. + */ + addEventListener: function(type, handler) { + cr.EventTarget.prototype.addEventListener.call(this, type, handler); + if (!(type in this.registeredPreferences_)) + this.registeredPreferences_[type] = {}; + }, + + /** + * Initializes preference reading and change notifications. + */ + initialize: function() { + var params1 = ['Preferences.prefsFetchedCallback']; + var params2 = ['Preferences.prefsChangedCallback']; + for (var prefName in this.registeredPreferences_) { + params1.push(prefName); + params2.push(prefName); + } + chrome.send('fetchPrefs', params1); + chrome.send('observePrefs', params2); + }, + + /** + * Helper function for flattening of dictionary passed via fetchPrefs + * callback. + * @param {string} prefix Preference name prefix. + * @param {object} dict Map with preference values. + * @private + */ + flattenMapAndDispatchEvent_: function(prefix, dict) { + for (var prefName in dict) { + if (typeof dict[prefName] == 'object' && + !this.registeredPreferences_[prefix + prefName]) { + this.flattenMapAndDispatchEvent_(prefix + prefName + '.', + dict[prefName]); + } else { + var event = new Event(prefix + prefName); + this.registeredPreferences_[prefix + prefName].orig = dict[prefName]; + event.value = dict[prefName]; + this.dispatchEvent(event); + } + } + }, + + /** + * Sets a preference and signals its new value. The change is propagated + * throughout the UI code but is not committed to Chrome yet. The new value + * and its data type are stored so that commitPref() can later be used to + * invoke the appropriate set*Pref() method and actually commit the change. + * @param {string} name Preference name. + * @param {string} type Preference data type. + * @param {*} value New preference value. + * @private + */ + setPrefNoCommit_: function(name, type, value) { + var pref = this.registeredPreferences_[name]; + pref.action = 'set'; + pref.type = type; + pref.value = value; + + var event = new Event(name); + // Decorate pref value as CoreOptionsHandler::CreateValueForPref() does. + event.value = { + value: value, + recommendedValue: pref.orig.recommendedValue, + disabled: pref.orig.disabled, + uncommitted: true, + }; + this.dispatchEvent(event); + }, + + /** + * Clears a preference and signals its new value. The change is propagated + * throughout the UI code but is not committed to Chrome yet. + * @param {string} name Preference name. + * @private + */ + clearPrefNoCommit_: function(name) { + var pref = this.registeredPreferences_[name]; + pref.action = 'clear'; + delete pref.type; + delete pref.value; + + var event = new Event(name); + // Decorate pref value as CoreOptionsHandler::CreateValueForPref() does. + event.value = { + value: pref.orig.recommendedValue, + controlledBy: 'recommended', + recommendedValue: pref.orig.recommendedValue, + disabled: pref.orig.disabled, + uncommitted: true, + }; + this.dispatchEvent(event); + }, + + /** + * Commits a preference change to Chrome and signals the new preference + * value. Does nothing if there is no uncommitted change. + * @param {string} name Preference name. + * @param {string} metric User metrics identifier. + */ + commitPref: function(name, metric) { + var pref = this.registeredPreferences_[name]; + switch (pref.action) { + case 'set': + switch (pref.type) { + case 'bool': + Preferences.setBooleanPref(name, pref.value, true, metric); + break; + case 'int': + Preferences.setIntegerPref(name, pref.value, true, metric); + break; + case 'double': + Preferences.setDoublePref(name, pref.value, true, metric); + break; + case 'string': + Preferences.setStringPref(name, pref.value, true, metric); + break; + case 'url': + Preferences.setURLPref(name, pref.value, true, metric); + break; + case 'list': + Preferences.setListPref(name, pref.value, true, metric); + break; + } + break; + case 'clear': + Preferences.clearPref(name, true, metric); + break; + } + delete pref.action; + delete pref.type; + delete pref.value; + }, + + /** + * Rolls back a preference change and signals the original preference value. + * Does nothing if there is no uncommitted change. + * @param {string} name Preference name. + */ + rollbackPref: function(name) { + var pref = this.registeredPreferences_[name]; + if (!pref.action) + return; + + delete pref.action; + delete pref.type; + delete pref.value; + + var event = new Event(name); + event.value = pref.orig; + event.value.uncommitted = true; + this.dispatchEvent(event); + } + }; + + /** + * Callback for fetchPrefs method. + * @param {object} dict Map of fetched property values. + */ + Preferences.prefsFetchedCallback = function(dict) { + Preferences.getInstance().flattenMapAndDispatchEvent_('', dict); + }; + + /** + * Callback for observePrefs method. + * @param {array} notification An array defining changed preference values. + * notification[0] contains name of the change preference while its new value + * is stored in notification[1]. + */ + Preferences.prefsChangedCallback = function(notification) { + var event = new Event(notification[0]); + event.value = notification[1]; + prefs = Preferences.getInstance(); + prefs.registeredPreferences_[notification[0]] = {orig: notification[1]}; + prefs.dispatchEvent(event); + }; + + // Export + return { + Preferences: Preferences + }; + +}); diff --git a/chromium/chrome/browser/resources/options/profiles_icon_grid.js b/chromium/chrome/browser/resources/options/profiles_icon_grid.js new file mode 100644 index 00000000000..a35b23fe436 --- /dev/null +++ b/chromium/chrome/browser/resources/options/profiles_icon_grid.js @@ -0,0 +1,68 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + /** @const */ var ListItem = cr.ui.ListItem; + /** @const */ var Grid = cr.ui.Grid; + /** @const */ var ListSingleSelectionModel = cr.ui.ListSingleSelectionModel; + + /** + * Creates a new profile icon grid item. + * @param {Object} iconURL The profile icon URL. + * @constructor + * @extends {cr.ui.GridItem} + */ + function ProfilesIconGridItem(iconURL) { + var el = cr.doc.createElement('span'); + el.iconURL_ = iconURL; + ProfilesIconGridItem.decorate(el); + return el; + } + + /** + * Decorates an element as a profile grid item. + * @param {!HTMLElement} el The element to decorate. + */ + ProfilesIconGridItem.decorate = function(el) { + el.__proto__ = ProfilesIconGridItem.prototype; + el.decorate(); + }; + + ProfilesIconGridItem.prototype = { + __proto__: ListItem.prototype, + + /** @override */ + decorate: function() { + ListItem.prototype.decorate.call(this); + var imageEl = cr.doc.createElement('img'); + imageEl.className = 'profile-icon'; + imageEl.style.content = imageset(this.iconURL_ + '@scalefactorx'); + this.appendChild(imageEl); + + this.className = 'profile-icon-grid-item'; + }, + }; + + var ProfilesIconGrid = cr.ui.define('grid'); + + ProfilesIconGrid.prototype = { + __proto__: Grid.prototype, + + /** @override */ + decorate: function() { + Grid.prototype.decorate.call(this); + this.selectionModel = new ListSingleSelectionModel(); + }, + + /** @override */ + createItem: function(iconURL) { + return new ProfilesIconGridItem(iconURL); + }, + }; + + return { + ProfilesIconGrid: ProfilesIconGrid + }; +}); + diff --git a/chromium/chrome/browser/resources/options/reset_profile_settings_banner.css b/chromium/chrome/browser/resources/options/reset_profile_settings_banner.css new file mode 100644 index 00000000000..21087f8aebd --- /dev/null +++ b/chromium/chrome/browser/resources/options/reset_profile_settings_banner.css @@ -0,0 +1,77 @@ +/* Copyright 2013 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ + +#reset-profile-settings-banner { + background-color: #f5f5f5; + border-color: #c8c8c8; + border-radius: 3px; + border-style: solid; + border-width: 1px; + margin-bottom: 24px; + margin-top: 20px; + position: relative; + width: 716px; +} + +#reset-profile-settings-banner > .close-button { + background-image: url('chrome://theme/IDR_CLOSE_DIALOG'); + background-position: center; + background-repeat: no-repeat; + height: 14px; + opacity: 0.5; + position: absolute; + right: 4px; + top: 4px; + width: 14px; +} + +html[dir='rtl'] #reset-profile-settings-banner > .close-button { + left: 4px; + right: auto; +} + +#reset-profile-settings-banner > .close-button:hover { + background-image: url('chrome://theme/IDR_CLOSE_DIALOG_H'); +} + +#reset-profile-settings-banner > .close-button:active { + background-image: url('chrome://theme/IDR_CLOSE_DIALOG_P'); +} + +#reset-profile-settings-banner .content-area { + -webkit-box-align: center; + display: -webkit-box; + padding: 17px; +} + +#reset-profile-settings-banner .content-area .badge { + background-image: url(yellow_gear.png); + background-position: center; + background-repeat: no-repeat; + height: 55px; + width: 58px; +} + +#reset-profile-settings-banner .content-area .text { + -webkit-box-flex: 1.0; + -webkit-margin-start: 18px; +} + +#reset-profile-settings-banner .content-area .text p { + -webkit-margin-after: 0; + -webkit-margin-before: 0; +} + +#reset-profile-settings-banner .content-area .button-area { + -webkit-margin-start: 54px; +} + +#reset-profile-settings-banner .nowrap { + white-space: nowrap; +} + +#reset-profile-settings-banner button { + margin-bottom: 1px; + margin-right: 0; +} diff --git a/chromium/chrome/browser/resources/options/reset_profile_settings_banner.html b/chromium/chrome/browser/resources/options/reset_profile_settings_banner.html new file mode 100644 index 00000000000..77b6d592eca --- /dev/null +++ b/chromium/chrome/browser/resources/options/reset_profile_settings_banner.html @@ -0,0 +1,18 @@ +<div id="reset-profile-settings-banner" hidden> + <div id="reset-profile-settings-banner-close" class="close-button"></div> + <div class="content-area"> + <div class="badge"></div> + <div class="text"> + <p> + <span i18n-values=".innerHTML:resetProfileSettingsBannerText"> + </span> + <a class="nowrap" i18n-values="href:resetProfileSettingsLearnMoreUrl" + i18n-content="learnMore" target="_blank"></a> + </p> + </div> + <div class="button-area"> + <button id="reset-profile-settings-banner-activate" + i18n-content="resetProfileSettings"></button> + </div> + </div> +</div> diff --git a/chromium/chrome/browser/resources/options/reset_profile_settings_banner.js b/chromium/chrome/browser/resources/options/reset_profile_settings_banner.js new file mode 100644 index 00000000000..4517e9151c7 --- /dev/null +++ b/chromium/chrome/browser/resources/options/reset_profile_settings_banner.js @@ -0,0 +1,103 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Note: the native-side handler for this is ResetProfileSettingsHandler. + +cr.define('options', function() { + /** @const */ var OptionsPage = options.OptionsPage; + + /** + * ResetProfileSettingsBanner class + * Provides encapsulated handling of the Reset Profile Settings banner. + * @constructor + */ + function ResetProfileSettingsBanner() {} + + cr.addSingletonGetter(ResetProfileSettingsBanner); + + ResetProfileSettingsBanner.prototype = { + /** + * Whether or not the banner has already been dismissed. + * + * This is needed because of the surprising ordering of asynchronous + * JS<->native calls when the settings page is opened with specifying a + * given sub-page, e.g. chrome://settings/resetProfileSettings. + * + * In such a case, ResetProfileSettingsOverlay's didShowPage(), which calls + * our dismiss() method, would be called before the native Handlers' + * InitalizePage() methods have an effect in the JS, which includes calling + * our show() method. This would mean that the banner would be first + * dismissed, then shown. We want to prevent this. + * + * @type {boolean} + * @private + */ + hadBeenDismissed_: false, + + /** + * Initializes the banner's event handlers. + */ + initialize: function() { + $('reset-profile-settings-banner-close').onclick = function(event) { + chrome.send('metricsHandler:recordAction', + ['AutomaticReset_WebUIBanner_ManuallyClosed']); + ResetProfileSettingsBanner.dismiss(); + }; + $('reset-profile-settings-banner-activate').onclick = function(event) { + chrome.send('metricsHandler:recordAction', + ['AutomaticReset_WebUIBanner_ResetClicked']); + OptionsPage.navigateToPage('resetProfileSettings'); + }; + }, + + /** + * Called by the native code to show the banner if needed. + * @private + */ + show_: function() { + if (!this.hadBeenDismissed_) { + chrome.send('metricsHandler:recordAction', + ['AutomaticReset_WebUIBanner_BannerShown']); + this.setVisibility_(true); + } + }, + + /** + * Called when the banner should be closed as a result of something taking + * place on the WebUI page, i.e. when its close button is pressed, or when + * the confirmation dialog for the profile settings reset feature is opened. + * @private + */ + dismiss_: function() { + chrome.send('onDismissedResetProfileSettingsBanner'); + this.hadBeenDismissed_ = true; + this.setVisibility_(false); + }, + + /** + * Sets whether or not the reset profile settings banner shall be visible. + * @param {boolean} show Whether or not to show the banner. + * @private + */ + setVisibility_: function(show) { + $('reset-profile-settings-banner').hidden = !show; + } + }; + + // Forward public APIs to private implementations. + [ + 'show', + 'dismiss', + ].forEach(function(name) { + ResetProfileSettingsBanner[name] = function() { + var instance = ResetProfileSettingsBanner.getInstance(); + return instance[name + '_'].apply(instance, arguments); + }; + }); + + // Export + return { + ResetProfileSettingsBanner: ResetProfileSettingsBanner + }; +}); diff --git a/chromium/chrome/browser/resources/options/reset_profile_settings_overlay.css b/chromium/chrome/browser/resources/options/reset_profile_settings_overlay.css new file mode 100644 index 00000000000..3614e0ab740 --- /dev/null +++ b/chromium/chrome/browser/resources/options/reset_profile_settings_overlay.css @@ -0,0 +1,62 @@ +/* Copyright 2013 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ + +#reset-profile-settings-overlay { + width: 500px; +} + +#reset-profile-settings-content-area { + -webkit-box-flex: 0; +} + +#reset-profile-settings-throbber { + margin: 4px 10px; + vertical-align: middle; + visibility: hidden; +} + +#feedback-bar { + -webkit-box-flex: 1; + -webkit-box-orient: vertical; +} + +#feedback-template { + -webkit-box-flex: 1; + overflow-y: auto; +} + +#feedback-template table { + table-layout: fixed; + width: 100%; +} + +#feedback-template table td { + word-wrap: break-word; +} + +#feedback-template .key { + padding-right: 5px; + text-align: right; + vertical-align: top; +} + +#feedback-template .value { + color: #333; + text-align: left; + white-space: pre-line; +} + +#expand-feedback { + background: center bottom no-repeat; + background-image: url('chrome://theme/IDR_QUESTION_MARK'); + display: inline-block; + height: 14px; + opacity: 0.33; + vertical-align: text-top; + width: 14px; +} + +#expand-feedback:hover { + opacity: 1; +}
\ No newline at end of file diff --git a/chromium/chrome/browser/resources/options/reset_profile_settings_overlay.html b/chromium/chrome/browser/resources/options/reset_profile_settings_overlay.html new file mode 100644 index 00000000000..e348c818d91 --- /dev/null +++ b/chromium/chrome/browser/resources/options/reset_profile_settings_overlay.html @@ -0,0 +1,45 @@ +<div id="reset-profile-settings-overlay" class="page" hidden> + <div class="close-button"></div> + <h1 i18n-content="resetProfileSettingsOverlay"></h1> + <div id="reset-profile-settings-content-area" class="content-area"> + <span i18n-content="resetProfileSettingsExplanation"></span> + </div> + <div class="action-area"> + <div class="hbox stretch"> + <a target="_blank" i18n-content="learnMore" + i18n-values="href:resetProfileSettingsLearnMoreUrl"> + </a> + </div> + <div class="action-area-right"> + <div id="reset-profile-settings-throbber" class="throbber"></div> + <div class="button-strip"> + <button id="reset-profile-settings-dismiss" i18n-content="cancel"> + </button> + <button id="reset-profile-settings-commit" + i18n-content="resetProfileSettingsCommit"> + </button> + </div> + </div> + </div> + <div id="feedback-bar" class="gray-bottom-bar checkbox"> + <span class="controlled-setting-with-label"> + <input id="send-settings" type="checkbox" checked> + <span> + <label for="send-settings" i18n-content="resetProfileSettingsFeedback"> + </label> + <div id='expand-feedback'></div> + </span> + </span> + <div id="feedback-template" hidden> + <div> + <table> + <tr jsselect="feedbackInfo"> + <td class="key"><span jscontent="key">KEY</span></td> + <td class="value"><span jscontent="value">VALUE</span></td> + </tr> + </table> + </div> + </div> + </div> +</div> +<script src="chrome://resources/js/jstemplate_compiled.js"></script>
\ No newline at end of file diff --git a/chromium/chrome/browser/resources/options/reset_profile_settings_overlay.js b/chromium/chrome/browser/resources/options/reset_profile_settings_overlay.js new file mode 100644 index 00000000000..ab4d830b714 --- /dev/null +++ b/chromium/chrome/browser/resources/options/reset_profile_settings_overlay.js @@ -0,0 +1,96 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + var OptionsPage = options.OptionsPage; + var ResetProfileSettingsBanner = options.ResetProfileSettingsBanner; + + /** + * ResetProfileSettingsOverlay class + * Encapsulated handling of the 'Reset Profile Settings' overlay page. + * @class + */ + function ResetProfileSettingsOverlay() { + OptionsPage.call( + this, 'resetProfileSettings', + loadTimeData.getString('resetProfileSettingsOverlayTabTitle'), + 'reset-profile-settings-overlay'); + } + + cr.addSingletonGetter(ResetProfileSettingsOverlay); + + ResetProfileSettingsOverlay.prototype = { + // Inherit ResetProfileSettingsOverlay from OptionsPage. + __proto__: OptionsPage.prototype, + + /** + * Initialize the page. + */ + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + + $('reset-profile-settings-dismiss').onclick = function(event) { + ResetProfileSettingsOverlay.dismiss(); + }; + $('reset-profile-settings-commit').onclick = function(event) { + ResetProfileSettingsOverlay.setResettingState(true); + chrome.send('performResetProfileSettings', + [$('send-settings').checked]); + }; + $('expand-feedback').onclick = function(event) { + var feedbackTemplate = $('feedback-template'); + feedbackTemplate.hidden = !feedbackTemplate.hidden; + }; + }, + + /** @override */ + didShowPage: function() { + ResetProfileSettingsBanner.dismiss(); + chrome.send('onShowResetProfileDialog'); + }, + }; + + /** + * Enables/disables UI elements after/while Chrome is performing a reset. + * @param {boolean} state If true, UI elements are disabled. + */ + ResetProfileSettingsOverlay.setResettingState = function(state) { + $('reset-profile-settings-throbber').style.visibility = + state ? 'visible' : 'hidden'; + $('reset-profile-settings-dismiss').disabled = state; + $('reset-profile-settings-commit').disabled = state; + }; + + /** + * Chrome callback to notify ResetProfileSettingsOverlay that the reset + * operation has terminated. + */ + ResetProfileSettingsOverlay.doneResetting = function() { + // The delay gives the user some feedback that the resetting + // actually worked. Otherwise the dialog just vanishes instantly in most + // cases. + window.setTimeout(function() { + ResetProfileSettingsOverlay.dismiss(); + }, 200); + }; + + /** + * Dismisses the overlay. + */ + ResetProfileSettingsOverlay.dismiss = function() { + OptionsPage.closeOverlay(); + ResetProfileSettingsOverlay.setResettingState(false); + }; + + ResetProfileSettingsOverlay.setFeedbackInfo = function(feedbackListData) { + var input = new JsEvalContext(feedbackListData); + var output = $('feedback-template'); + jstProcess(input, output); + }; + + // Export + return { + ResetProfileSettingsOverlay: ResetProfileSettingsOverlay + }; +}); diff --git a/chromium/chrome/browser/resources/options/search_box.html b/chromium/chrome/browser/resources/options/search_box.html new file mode 100644 index 00000000000..f27e1e7bc56 --- /dev/null +++ b/chromium/chrome/browser/resources/options/search_box.html @@ -0,0 +1,10 @@ +<div id="searchBox" class="page"> + <header > + <span id="browser-options-search-field-container" + class="search-field-container"> + <input id="search-field" type="search" + i18n-values="placeholder:searchPlaceholder; + aria-label:searchPlaceholder" incremental> + </span> + </header> +</div> diff --git a/chromium/chrome/browser/resources/options/search_engine_manager.css b/chromium/chrome/browser/resources/options/search_engine_manager.css new file mode 100644 index 00000000000..c19734da4e4 --- /dev/null +++ b/chromium/chrome/browser/resources/options/search_engine_manager.css @@ -0,0 +1,81 @@ +/* Copyright (c) 2012 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ + +#search-engine-manager-page { + width: 700px; +} + +.search-engine-list input { + -webkit-box-flex: 1; + display: -webkit-box; +} + +.search-engine-list > div { + display: -webkit-box; +} + +.search-engine-list .favicon { + background-position: center center; + background-repeat: no-repeat; + background-size: 16px; + height: 16px; + line-height: 16px; + padding: 0 7px; + width: 16px; +} + +.search-engine-list .name-column { + -webkit-box-align: center; + -webkit-padding-end: 1ex; + box-sizing: border-box; + display: -webkit-box; + width: 30%; +} + +.search-engine-list .name-column :last-child { + -webkit-box-flex: 1; +} + +.search-engine-list .keyword-column { + -webkit-padding-end: 1ex; + box-sizing: border-box; + width: 26%; +} + +.search-engine-list .url-column { + box-sizing: border-box; + width: 44%; +} + +.search-engine-list .keyword-column, +.search-engine-list .url-column { + color: #666; +} + +.search-engine-list .default .name-column, +.search-engine-list .default .keyword-column { + font-weight: bold; +} + +/* For temporary Make Default button */ +.search-engine-list .url-column { + -webkit-box-align: center; + display: -webkit-box; +} + +.search-engine-list .url-column :first-child { + -webkit-box-flex: 1; +} + +.search-engine-list .url-column .list-inline-button { + margin-top: 0; + padding: 1px 6px 2px 6px; +} + +.search-engine-list > :not(:hover):not([editing]) .url-column + .list-inline-button { + display: none; +} + +/* End temporary Make Default button styling */ diff --git a/chromium/chrome/browser/resources/options/search_engine_manager.html b/chromium/chrome/browser/resources/options/search_engine_manager.html new file mode 100644 index 00000000000..0f9d8566d69 --- /dev/null +++ b/chromium/chrome/browser/resources/options/search_engine_manager.html @@ -0,0 +1,25 @@ +<div id="search-engine-manager-page" class="page" hidden> + <div class="close-button"></div> + <h1 i18n-content="searchEngineManagerPage"></h1> + <div class="content-area"> + <h3 i18n-content="defaultSearchEngineListTitle"></h3> + <list id="default-search-engine-list" + class="search-engine-list settings-list"></list> + <h3 i18n-content="otherSearchEngineListTitle"></h3> + <list id="other-search-engine-list" + class="search-engine-list settings-list"></list> + <div id="extension-keyword-div" hidden> + <h3 id="extension-keyword-list-title" + i18n-content="extensionKeywordsListTitle"></h3> + <list id="extension-keyword-list" + class="search-engine-list settings-list"></list> + </div> + </div> + <div class="action-area"> + <div class="button-strip"> + <button id="search-engine-manager-confirm" type="submit" + class="default-button" i18n-content="done"> + </button> + </div> + </div> +</div> diff --git a/chromium/chrome/browser/resources/options/search_engine_manager.js b/chromium/chrome/browser/resources/options/search_engine_manager.js new file mode 100644 index 00000000000..7d8df03c1ae --- /dev/null +++ b/chromium/chrome/browser/resources/options/search_engine_manager.js @@ -0,0 +1,131 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + /** @const */ var OptionsPage = options.OptionsPage; + /** @const */ var ArrayDataModel = cr.ui.ArrayDataModel; + + /** + * Encapsulated handling of search engine management page. + * @constructor + */ + function SearchEngineManager() { + this.activeNavTab = null; + OptionsPage.call(this, 'searchEngines', + loadTimeData.getString('searchEngineManagerPageTabTitle'), + 'search-engine-manager-page'); + } + + cr.addSingletonGetter(SearchEngineManager); + + SearchEngineManager.prototype = { + __proto__: OptionsPage.prototype, + + /** + * List for default search engine options. + * @private + */ + defaultsList_: null, + + /** + * List for other search engine options. + * @private + */ + othersList_: null, + + /** + * List for extension keywords. + * @private + */ + extensionList_: null, + + /** inheritDoc */ + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + + this.defaultsList_ = $('default-search-engine-list'); + this.setUpList_(this.defaultsList_); + + this.othersList_ = $('other-search-engine-list'); + this.setUpList_(this.othersList_); + + this.extensionList_ = $('extension-keyword-list'); + this.setUpList_(this.extensionList_); + + $('search-engine-manager-confirm').onclick = function() { + OptionsPage.closeOverlay(); + }; + }, + + /** + * Sets up the given list as a search engine list + * @param {List} list The list to set up. + * @private + */ + setUpList_: function(list) { + options.search_engines.SearchEngineList.decorate(list); + list.autoExpands = true; + }, + + /** + * Updates the search engine list with the given entries. + * @private + * @param {Array} defaultEngines List of possible default search engines. + * @param {Array} otherEngines List of other search engines. + * @param {Array} keywords List of keywords from extensions. + */ + updateSearchEngineList_: function(defaultEngines, otherEngines, keywords) { + this.defaultsList_.dataModel = new ArrayDataModel(defaultEngines); + + otherEngines = otherEngines.map(function(x) { + return [x, x.name.toLocaleLowerCase()]; + }).sort(function(a, b) { + return a[1].localeCompare(b[1]); + }).map(function(x) { + return x[0]; + }); + + var othersModel = new ArrayDataModel(otherEngines); + // Add a "new engine" row. + othersModel.push({ + 'modelIndex': '-1', + 'canBeEdited': true + }); + this.othersList_.dataModel = othersModel; + + if (keywords.length > 0) { + $('extension-keyword-div').hidden = false; + var extensionsModel = new ArrayDataModel(keywords); + this.extensionList_.dataModel = extensionsModel; + } else { + $('extension-keyword-div').hidden = true; + } + }, + }; + + SearchEngineManager.updateSearchEngineList = function(defaultEngines, + otherEngines, + keywords) { + SearchEngineManager.getInstance().updateSearchEngineList_(defaultEngines, + otherEngines, + keywords); + }; + + SearchEngineManager.validityCheckCallback = function(validity, modelIndex) { + // Forward to all lists; those without a matching modelIndex will ignore it. + SearchEngineManager.getInstance().defaultsList_.validationComplete( + validity, modelIndex); + SearchEngineManager.getInstance().othersList_.validationComplete( + validity, modelIndex); + SearchEngineManager.getInstance().extensionList_.validationComplete( + validity, modelIndex); + }; + + // Export + return { + SearchEngineManager: SearchEngineManager + }; + +}); + diff --git a/chromium/chrome/browser/resources/options/search_engine_manager_engine_list.js b/chromium/chrome/browser/resources/options/search_engine_manager_engine_list.js new file mode 100644 index 00000000000..9bc12b9f8b6 --- /dev/null +++ b/chromium/chrome/browser/resources/options/search_engine_manager_engine_list.js @@ -0,0 +1,343 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options.search_engines', function() { + /** @const */ var ControlledSettingIndicator = + options.ControlledSettingIndicator; + /** @const */ var InlineEditableItemList = options.InlineEditableItemList; + /** @const */ var InlineEditableItem = options.InlineEditableItem; + /** @const */ var ListSelectionController = cr.ui.ListSelectionController; + + /** + * Creates a new search engine list item. + * @param {Object} searchEnigne The search engine this represents. + * @constructor + * @extends {cr.ui.ListItem} + */ + function SearchEngineListItem(searchEngine) { + var el = cr.doc.createElement('div'); + el.searchEngine_ = searchEngine; + SearchEngineListItem.decorate(el); + return el; + } + + /** + * Decorates an element as a search engine list item. + * @param {!HTMLElement} el The element to decorate. + */ + SearchEngineListItem.decorate = function(el) { + el.__proto__ = SearchEngineListItem.prototype; + el.decorate(); + }; + + SearchEngineListItem.prototype = { + __proto__: InlineEditableItem.prototype, + + /** + * Input field for editing the engine name. + * @type {HTMLElement} + * @private + */ + nameField_: null, + + /** + * Input field for editing the engine keyword. + * @type {HTMLElement} + * @private + */ + keywordField_: null, + + /** + * Input field for editing the engine url. + * @type {HTMLElement} + * @private + */ + urlField_: null, + + /** + * Whether or not an input validation request is currently outstanding. + * @type {boolean} + * @private + */ + waitingForValidation_: false, + + /** + * Whether or not the current set of input is known to be valid. + * @type {boolean} + * @private + */ + currentlyValid_: false, + + /** @override */ + decorate: function() { + InlineEditableItem.prototype.decorate.call(this); + + var engine = this.searchEngine_; + + if (engine.modelIndex == '-1') { + this.isPlaceholder = true; + engine.name = ''; + engine.keyword = ''; + engine.url = ''; + } + + this.currentlyValid_ = !this.isPlaceholder; + + if (engine.default) + this.classList.add('default'); + + this.deletable = engine.canBeRemoved; + + // Construct the name column. + var nameColEl = this.ownerDocument.createElement('div'); + nameColEl.className = 'name-column'; + nameColEl.classList.add('weakrtl'); + this.contentElement.appendChild(nameColEl); + + // Add the favicon. + var faviconDivEl = this.ownerDocument.createElement('div'); + faviconDivEl.className = 'favicon'; + if (!this.isPlaceholder) { + faviconDivEl.style.backgroundImage = imageset( + 'chrome://favicon/size/16@scalefactorx/iconurl/' + engine.iconURL); + } + nameColEl.appendChild(faviconDivEl); + + var nameEl = this.createEditableTextCell(engine.displayName); + nameEl.classList.add('weakrtl'); + nameColEl.appendChild(nameEl); + + // Then the keyword column. + var keywordEl = this.createEditableTextCell(engine.keyword); + keywordEl.className = 'keyword-column'; + keywordEl.classList.add('weakrtl'); + this.contentElement.appendChild(keywordEl); + + // And the URL column. + var urlEl = this.createEditableTextCell(engine.url); + // Extensions should not display a URL column. + if (!engine.isExtension) { + var urlWithButtonEl = this.ownerDocument.createElement('div'); + urlWithButtonEl.appendChild(urlEl); + urlWithButtonEl.className = 'url-column'; + urlWithButtonEl.classList.add('weakrtl'); + this.contentElement.appendChild(urlWithButtonEl); + // Add the Make Default button. Temporary until drag-and-drop + // re-ordering is implemented. When this is removed, remove the extra + // div above. + if (engine.canBeDefault) { + var makeDefaultButtonEl = this.ownerDocument.createElement('button'); + makeDefaultButtonEl.className = + 'custom-appearance list-inline-button'; + makeDefaultButtonEl.textContent = + loadTimeData.getString('makeDefaultSearchEngineButton'); + makeDefaultButtonEl.onclick = function(e) { + chrome.send('managerSetDefaultSearchEngine', [engine.modelIndex]); + }; + makeDefaultButtonEl.onmousedown = function(e) { + // Don't select the row when clicking the button. + e.stopPropagation(); + // Don't focus on the button. + e.preventDefault(); + }; + urlWithButtonEl.appendChild(makeDefaultButtonEl); + } + } + + // Do final adjustment to the input fields. + this.nameField_ = nameEl.querySelector('input'); + // The editable field uses the raw name, not the display name. + this.nameField_.value = engine.name; + this.keywordField_ = keywordEl.querySelector('input'); + this.urlField_ = urlEl.querySelector('input'); + + if (engine.urlLocked) + this.urlField_.disabled = true; + + if (engine.isExtension) + this.nameField_.disabled = true; + + if (this.isPlaceholder) { + this.nameField_.placeholder = + loadTimeData.getString('searchEngineTableNamePlaceholder'); + this.keywordField_.placeholder = + loadTimeData.getString('searchEngineTableKeywordPlaceholder'); + this.urlField_.placeholder = + loadTimeData.getString('searchEngineTableURLPlaceholder'); + } + + var fields = [this.nameField_, this.keywordField_, this.urlField_]; + for (var i = 0; i < fields.length; i++) { + fields[i].oninput = this.startFieldValidation_.bind(this); + } + + // Listen for edit events. + if (engine.canBeEdited) { + this.addEventListener('edit', this.onEditStarted_.bind(this)); + this.addEventListener('canceledit', this.onEditCancelled_.bind(this)); + this.addEventListener('commitedit', this.onEditCommitted_.bind(this)); + } else { + this.editable = false; + this.querySelector('.row-delete-button').hidden = true; + var indicator = ControlledSettingIndicator(); + indicator.setAttribute('setting', 'search-engine'); + // Create a synthetic pref change event decorated as + // CoreOptionsHandler::CreateValueForPref() does. + var event = new Event(this.contentType); + if (engine.extension) { + event.value = { controlledBy: 'extension' }; + // TODO(mad): add id, name, and icon once we solved the issue with the + // search engine manager in http://crbug.com/314507. + } else { + event.value = { controlledBy: 'policy' }; + } + indicator.handlePrefChange(event); + this.appendChild(indicator); + } + }, + + /** @override */ + get currentInputIsValid() { + return !this.waitingForValidation_ && this.currentlyValid_; + }, + + /** @override */ + get hasBeenEdited() { + var engine = this.searchEngine_; + return this.nameField_.value != engine.name || + this.keywordField_.value != engine.keyword || + this.urlField_.value != engine.url; + }, + + /** + * Called when entering edit mode; starts an edit session in the model. + * @param {Event} e The edit event. + * @private + */ + onEditStarted_: function(e) { + var editIndex = this.searchEngine_.modelIndex; + chrome.send('editSearchEngine', [String(editIndex)]); + this.startFieldValidation_(); + }, + + /** + * Called when committing an edit; updates the model. + * @param {Event} e The end event. + * @private + */ + onEditCommitted_: function(e) { + chrome.send('searchEngineEditCompleted', this.getInputFieldValues_()); + }, + + /** + * Called when cancelling an edit; informs the model and resets the control + * states. + * @param {Event} e The cancel event. + * @private + */ + onEditCancelled_: function() { + chrome.send('searchEngineEditCancelled'); + + // The name field has been automatically set to match the display name, + // but it should use the raw name instead. + this.nameField_.value = this.searchEngine_.name; + this.currentlyValid_ = !this.isPlaceholder; + }, + + /** + * Returns the input field values as an array suitable for passing to + * chrome.send. The order of the array is important. + * @private + * @return {array} The current input field values. + */ + getInputFieldValues_: function() { + return [this.nameField_.value, + this.keywordField_.value, + this.urlField_.value]; + }, + + /** + * Begins the process of asynchronously validing the input fields. + * @private + */ + startFieldValidation_: function() { + this.waitingForValidation_ = true; + var args = this.getInputFieldValues_(); + args.push(this.searchEngine_.modelIndex); + chrome.send('checkSearchEngineInfoValidity', args); + }, + + /** + * Callback for the completion of an input validition check. + * @param {Object} validity A dictionary of validitation results. + */ + validationComplete: function(validity) { + this.waitingForValidation_ = false; + // TODO(stuartmorgan): Implement the full validation UI with + // checkmark/exclamation mark icons and tooltips showing the errors. + if (validity.name) { + this.nameField_.setCustomValidity(''); + } else { + this.nameField_.setCustomValidity( + loadTimeData.getString('editSearchEngineInvalidTitleToolTip')); + } + + if (validity.keyword) { + this.keywordField_.setCustomValidity(''); + } else { + this.keywordField_.setCustomValidity( + loadTimeData.getString('editSearchEngineInvalidKeywordToolTip')); + } + + if (validity.url) { + this.urlField_.setCustomValidity(''); + } else { + this.urlField_.setCustomValidity( + loadTimeData.getString('editSearchEngineInvalidURLToolTip')); + } + + this.currentlyValid_ = validity.name && validity.keyword && validity.url; + }, + }; + + var SearchEngineList = cr.ui.define('list'); + + SearchEngineList.prototype = { + __proto__: InlineEditableItemList.prototype, + + /** @override */ + createItem: function(searchEngine) { + return new SearchEngineListItem(searchEngine); + }, + + /** @override */ + deleteItemAtIndex: function(index) { + var modelIndex = this.dataModel.item(index).modelIndex; + chrome.send('removeSearchEngine', [String(modelIndex)]); + }, + + /** + * Passes the results of an input validation check to the requesting row + * if it's still being edited. + * @param {number} modelIndex The model index of the item that was checked. + * @param {Object} validity A dictionary of validitation results. + */ + validationComplete: function(validity, modelIndex) { + // If it's not still being edited, it no longer matters. + var currentSelection = this.selectedItem; + if (!currentSelection) + return; + var listItem = this.getListItem(currentSelection); + if (listItem.editing && currentSelection.modelIndex == modelIndex) + listItem.validationComplete(validity); + }, + }; + + // Export + return { + SearchEngineList: SearchEngineList + }; + +}); + diff --git a/chromium/chrome/browser/resources/options/search_page.css b/chromium/chrome/browser/resources/options/search_page.css new file mode 100644 index 00000000000..b634928989a --- /dev/null +++ b/chromium/chrome/browser/resources/options/search_page.css @@ -0,0 +1,74 @@ +/* Copyright (c) 2012 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ + +.search-hidden { + display: none !important; +} + +.search-highlighted { + background-color: rgba(255, 240, 120, 0.9); +} + +/* Container for the elements that make up the search bubble. */ +.search-bubble { + left: 0; + margin-top: 5px; + pointer-events: none; + position: absolute; + top: -1000px; /* Minor hack: position off-screen by default. */ + /* Create a z-context for search-bubble-innards, its after and before. */ + z-index: 1; +} + +/* Contains the text content of the bubble. */ +.search-bubble-innards { + background: -webkit-linear-gradient(rgba(255, 248, 172, 0.9), + rgba(255, 243, 128, 0.9)); + border-radius: 2px; + padding: 4px 10px; + text-align: center; + width: 100px; +} + +/* Provides the border around the bubble (has to be behind ::after). */ +.search-bubble-innards::before { + border: 1px solid rgb(220, 198, 72); + border-radius: 2px; + bottom: -1px; + content: ''; + left: -1px; + position: absolute; + right: -1px; + top: -1px; + z-index: -2; +} + +/* Provides the arrow which points at the anchor element. */ +.search-bubble-innards::after { + -webkit-transform: rotate(45deg); + background: + -webkit-linear-gradient(-45deg, rgb(251, 255, 181), + rgb(255, 248, 172) 50%, + rgba(255, 248, 172, 0)); + border: 1px solid rgb(220, 198, 72); + border-bottom-color: transparent; + border-right-color: transparent; + content: ''; + height: 12px; + left: 53px; + position: absolute; + top: -7px; + width: 12px; + z-index: -1; +} + +.search-bubble-wrapper { + position: relative; +} + +/* #mainview is here to win specificity. :( */ +#mainview #searchPage.page, +#mainview #searchBox.page { + padding-bottom: 0; +} diff --git a/chromium/chrome/browser/resources/options/search_page.html b/chromium/chrome/browser/resources/options/search_page.html new file mode 100644 index 00000000000..073c505d9f7 --- /dev/null +++ b/chromium/chrome/browser/resources/options/search_page.html @@ -0,0 +1,12 @@ +<div id="searchPage" class="page" hidden> + <header> + <h1 i18n-content="searchPage"></h1> + </header> + <div id="searchPageNoMatches" hidden> + <p i18n-content="searchPageNoMatches"></p> + <p><span i18n-content="searchPageHelpLabel"></span> + <a target="_blank" i18n-content="searchPageHelpTitle" + i18n-values="href:searchPageHelpURL"></a> + </p> + </div> +</div> diff --git a/chromium/chrome/browser/resources/options/search_page.js b/chromium/chrome/browser/resources/options/search_page.js new file mode 100644 index 00000000000..234399d7af2 --- /dev/null +++ b/chromium/chrome/browser/resources/options/search_page.js @@ -0,0 +1,572 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + /** @const */ var OptionsPage = options.OptionsPage; + + /** + * Encapsulated handling of a search bubble. + * @constructor + */ + function SearchBubble(text) { + var el = cr.doc.createElement('div'); + SearchBubble.decorate(el); + el.content = text; + return el; + } + + SearchBubble.decorate = function(el) { + el.__proto__ = SearchBubble.prototype; + el.decorate(); + }; + + SearchBubble.prototype = { + __proto__: HTMLDivElement.prototype, + + decorate: function() { + this.className = 'search-bubble'; + + this.innards_ = cr.doc.createElement('div'); + this.innards_.className = 'search-bubble-innards'; + this.appendChild(this.innards_); + + // We create a timer to periodically update the position of the bubbles. + // While this isn't all that desirable, it's the only sure-fire way of + // making sure the bubbles stay in the correct location as sections + // may dynamically change size at any time. + this.intervalId = setInterval(this.updatePosition.bind(this), 250); + }, + + /** + * Sets the text message in the bubble. + * @param {string} text The text the bubble will show. + */ + set content(text) { + this.innards_.textContent = text; + }, + + /** + * Attach the bubble to the element. + */ + attachTo: function(element) { + var parent = element.parentElement; + if (!parent) + return; + if (parent.tagName == 'TD') { + // To make absolute positioning work inside a table cell we need + // to wrap the bubble div into another div with position:relative. + // This only works properly if the element is the first child of the + // table cell which is true for all options pages. + this.wrapper = cr.doc.createElement('div'); + this.wrapper.className = 'search-bubble-wrapper'; + this.wrapper.appendChild(this); + parent.insertBefore(this.wrapper, element); + } else { + parent.insertBefore(this, element); + } + }, + + /** + * Clear the interval timer and remove the element from the page. + */ + dispose: function() { + clearInterval(this.intervalId); + + var child = this.wrapper || this; + var parent = child.parentNode; + if (parent) + parent.removeChild(child); + }, + + /** + * Update the position of the bubble. Called at creation time and then + * periodically while the bubble remains visible. + */ + updatePosition: function() { + // This bubble is 'owned' by the next sibling. + var owner = (this.wrapper || this).nextSibling; + + // If there isn't an offset parent, we have nothing to do. + if (!owner.offsetParent) + return; + + // Position the bubble below the location of the owner. + var left = owner.offsetLeft + owner.offsetWidth / 2 - + this.offsetWidth / 2; + var top = owner.offsetTop + owner.offsetHeight; + + // Update the position in the CSS. Cache the last values for + // best performance. + if (left != this.lastLeft) { + this.style.left = left + 'px'; + this.lastLeft = left; + } + if (top != this.lastTop) { + this.style.top = top + 'px'; + this.lastTop = top; + } + }, + }; + + /** + * Encapsulated handling of the search page. + * @constructor + */ + function SearchPage() { + OptionsPage.call(this, 'search', + loadTimeData.getString('searchPageTabTitle'), + 'searchPage'); + } + + cr.addSingletonGetter(SearchPage); + + SearchPage.prototype = { + // Inherit SearchPage from OptionsPage. + __proto__: OptionsPage.prototype, + + /** + * A boolean to prevent recursion. Used by setSearchText_(). + * @type {boolean} + * @private + */ + insideSetSearchText_: false, + + /** + * Initialize the page. + */ + initializePage: function() { + // Call base class implementation to start preference initialization. + OptionsPage.prototype.initializePage.call(this); + + this.searchField = $('search-field'); + + // Handle search events. (No need to throttle, WebKit's search field + // will do that automatically.) + this.searchField.onsearch = function(e) { + this.setSearchText_(e.currentTarget.value); + }.bind(this); + + // Install handler for key presses. + document.addEventListener('keydown', + this.keyDownEventHandler_.bind(this)); + }, + + /** @override */ + get sticky() { + return true; + }, + + /** + * Called after this page has shown. + */ + didShowPage: function() { + // This method is called by the Options page after all pages have + // had their visibilty attribute set. At this point we can perform the + // search specific DOM manipulation. + this.setSearchActive_(true); + }, + + /** + * Called before this page will be hidden. + */ + willHidePage: function() { + // This method is called by the Options page before all pages have + // their visibilty attribute set. Before that happens, we need to + // undo the search specific DOM manipulation that was performed in + // didShowPage. + this.setSearchActive_(false); + }, + + /** + * Update the UI to reflect whether we are in a search state. + * @param {boolean} active True if we are on the search page. + * @private + */ + setSearchActive_: function(active) { + // It's fine to exit if search wasn't active and we're not going to + // activate it now. + if (!this.searchActive_ && !active) + return; + + this.searchActive_ = active; + + if (active) { + var hash = location.hash; + if (hash) { + this.searchField.value = + decodeURIComponent(hash.slice(1).replace(/\+/g, ' ')); + } else if (!this.searchField.value) { + // This should only happen if the user goes directly to + // chrome://settings-frame/search + OptionsPage.showDefaultPage(); + return; + } + + // Move 'advanced' sections into the main settings page to allow + // searching. + if (!this.advancedSections_) { + this.advancedSections_ = + $('advanced-settings-container').querySelectorAll('section'); + for (var i = 0, section; section = this.advancedSections_[i]; i++) + $('settings').appendChild(section); + } + } + + var pagesToSearch = this.getSearchablePages_(); + for (var key in pagesToSearch) { + var page = pagesToSearch[key]; + + if (!active) + page.visible = false; + + // Update the visible state of all top-level elements that are not + // sections (ie titles, button strips). We do this before changing + // the page visibility to avoid excessive re-draw. + for (var i = 0, childDiv; childDiv = page.pageDiv.children[i]; i++) { + if (active) { + if (childDiv.tagName != 'SECTION') + childDiv.classList.add('search-hidden'); + } else { + childDiv.classList.remove('search-hidden'); + } + } + + if (active) { + // When search is active, remove the 'hidden' tag. This tag may have + // been added by the OptionsPage. + page.pageDiv.hidden = false; + } + } + + if (active) { + this.setSearchText_(this.searchField.value); + this.searchField.focus(); + } else { + // After hiding all page content, remove any search results. + this.unhighlightMatches_(); + this.removeSearchBubbles_(); + + // Move 'advanced' sections back into their original container. + if (this.advancedSections_) { + for (var i = 0, section; section = this.advancedSections_[i]; i++) + $('advanced-settings-container').appendChild(section); + this.advancedSections_ = null; + } + } + }, + + /** + * Set the current search criteria. + * @param {string} text Search text. + * @private + */ + setSearchText_: function(text) { + // Prevent recursive execution of this method. + if (this.insideSetSearchText_) return; + this.insideSetSearchText_ = true; + + // Cleanup the search query string. + text = SearchPage.canonicalizeQuery(text); + + // Set the hash on the current page, and the enclosing uber page + var hash = text ? '#' + encodeURIComponent(text) : ''; + var path = text ? this.name : ''; + window.location.hash = hash; + uber.invokeMethodOnParent('setPath', {path: path + hash}); + + // Toggle the search page if necessary. + if (text) { + if (!this.searchActive_) + OptionsPage.showPageByName(this.name, false); + } else { + if (this.searchActive_) + OptionsPage.showPageByName(OptionsPage.getDefaultPage().name, false); + + this.insideSetSearchText_ = false; + return; + } + + var foundMatches = false; + + // Remove any prior search results. + this.unhighlightMatches_(); + this.removeSearchBubbles_(); + + var pagesToSearch = this.getSearchablePages_(); + for (var key in pagesToSearch) { + var page = pagesToSearch[key]; + var elements = page.pageDiv.querySelectorAll('section'); + for (var i = 0, node; node = elements[i]; i++) { + node.classList.add('search-hidden'); + } + } + + var bubbleControls = []; + + // Generate search text by applying lowercase and escaping any characters + // that would be problematic for regular expressions. + var searchText = + text.toLowerCase().replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); + // Generate a regular expression for hilighting search terms. + var regExp = new RegExp('(' + searchText + ')', 'ig'); + + if (searchText.length) { + // Search all top-level sections for anchored string matches. + for (var key in pagesToSearch) { + var page = pagesToSearch[key]; + var elements = + page.pageDiv.querySelectorAll('section'); + for (var i = 0, node; node = elements[i]; i++) { + if (this.highlightMatches_(regExp, node)) { + node.classList.remove('search-hidden'); + if (!node.hidden) + foundMatches = true; + } + } + } + + // Search all sub-pages, generating an array of top-level sections that + // we need to make visible. + var subPagesToSearch = this.getSearchableSubPages_(); + var control, node; + for (var key in subPagesToSearch) { + var page = subPagesToSearch[key]; + if (this.highlightMatches_(regExp, page.pageDiv)) { + this.revealAssociatedSections_(page); + + bubbleControls = + bubbleControls.concat(this.getAssociatedControls_(page)); + + foundMatches = true; + } + } + } + + // Configure elements on the search results page based on search results. + $('searchPageNoMatches').hidden = foundMatches; + + // Create search balloons for sub-page results. + length = bubbleControls.length; + for (var i = 0; i < length; i++) + this.createSearchBubble_(bubbleControls[i], text); + + // Cleanup the recursion-prevention variable. + this.insideSetSearchText_ = false; + }, + + /** + * Reveal the associated section for |subpage|, as well as the one for its + * |parentPage|, and its |parentPage|'s |parentPage|, etc. + * @private + */ + revealAssociatedSections_: function(subpage) { + for (var page = subpage; page; page = page.parentPage) { + var section = page.associatedSection; + if (section) + section.classList.remove('search-hidden'); + } + }, + + /** + * @return {!Array.<HTMLElement>} all the associated controls for |subpage|, + * including |subpage.associatedControls| as well as any controls on parent + * pages that are indirectly necessary to get to the subpage. + * @private + */ + getAssociatedControls_: function(subpage) { + var controls = []; + for (var page = subpage; page; page = page.parentPage) { + if (page.associatedControls) + controls = controls.concat(page.associatedControls); + } + return controls; + }, + + /** + * Wraps matches in spans. + * @param {RegExp} regExp The search query (in regexp form). + * @param {Element} element An HTML container element to recursively search + * within. + * @return {boolean} true if the element was changed. + * @private + */ + highlightMatches_: function(regExp, element) { + var found = false; + var div, child, tmp; + + // Walk the tree, searching each TEXT node. + var walker = document.createTreeWalker(element, + NodeFilter.SHOW_TEXT, + null, + false); + var node = walker.nextNode(); + while (node) { + var textContent = node.nodeValue; + // Perform a search and replace on the text node value. + var split = textContent.split(regExp); + if (split.length > 1) { + found = true; + var nextNode = walker.nextNode(); + var parentNode = node.parentNode; + // Use existing node as placeholder to determine where to insert the + // replacement content. + for (var i = 0; i < split.length; ++i) { + if (i % 2 == 0) { + parentNode.insertBefore(document.createTextNode(split[i]), node); + } else { + var span = document.createElement('span'); + span.className = 'search-highlighted'; + span.textContent = split[i]; + parentNode.insertBefore(span, node); + } + } + // Remove old node. + parentNode.removeChild(node); + node = nextNode; + } else { + node = walker.nextNode(); + } + } + + return found; + }, + + /** + * Removes all search highlight tags from the document. + * @private + */ + unhighlightMatches_: function() { + // Find all search highlight elements. + var elements = document.querySelectorAll('.search-highlighted'); + + // For each element, remove the highlighting. + var parent, i; + for (var i = 0, node; node = elements[i]; i++) { + parent = node.parentNode; + + // Replace the highlight element with the first child (the text node). + parent.replaceChild(node.firstChild, node); + + // Normalize the parent so that multiple text nodes will be combined. + parent.normalize(); + } + }, + + /** + * Creates a search result bubble attached to an element. + * @param {Element} element An HTML element, usually a button. + * @param {string} text A string to show in the bubble. + * @private + */ + createSearchBubble_: function(element, text) { + // avoid appending multiple bubbles to a button. + var sibling = element.previousElementSibling; + if (sibling && (sibling.classList.contains('search-bubble') || + sibling.classList.contains('search-bubble-wrapper'))) + return; + + var parent = element.parentElement; + if (parent) { + var bubble = new SearchBubble(text); + bubble.attachTo(element); + bubble.updatePosition(); + } + }, + + /** + * Removes all search match bubbles. + * @private + */ + removeSearchBubbles_: function() { + var elements = document.querySelectorAll('.search-bubble'); + var length = elements.length; + for (var i = 0; i < length; i++) + elements[i].dispose(); + }, + + /** + * Builds a list of top-level pages to search. Omits the search page and + * all sub-pages. + * @return {Array} An array of pages to search. + * @private + */ + getSearchablePages_: function() { + var name, page, pages = []; + for (name in OptionsPage.registeredPages) { + if (name != this.name) { + page = OptionsPage.registeredPages[name]; + if (!page.parentPage) + pages.push(page); + } + } + return pages; + }, + + /** + * Builds a list of sub-pages (and overlay pages) to search. Ignore pages + * that have no associated controls. + * @return {Array} An array of pages to search. + * @private + */ + getSearchableSubPages_: function() { + var name, pageInfo, page, pages = []; + for (name in OptionsPage.registeredPages) { + page = OptionsPage.registeredPages[name]; + if (page.parentPage && page.associatedSection) + pages.push(page); + } + for (name in OptionsPage.registeredOverlayPages) { + page = OptionsPage.registeredOverlayPages[name]; + if (page.associatedSection && page.pageDiv != undefined) + pages.push(page); + } + return pages; + }, + + /** + * A function to handle key press events. + * @return {Event} a keydown event. + * @private + */ + keyDownEventHandler_: function(event) { + /** @const */ var ESCAPE_KEY_CODE = 27; + /** @const */ var FORWARD_SLASH_KEY_CODE = 191; + + switch (event.keyCode) { + case ESCAPE_KEY_CODE: + if (event.target == this.searchField) { + this.setSearchText_(''); + this.searchField.blur(); + event.stopPropagation(); + event.preventDefault(); + } + break; + case FORWARD_SLASH_KEY_CODE: + if (!/INPUT|SELECT|BUTTON|TEXTAREA/.test(event.target.tagName) && + !event.ctrlKey && !event.altKey) { + this.searchField.focus(); + event.stopPropagation(); + event.preventDefault(); + } + break; + } + }, + }; + + /** + * Standardizes a user-entered text query by removing extra whitespace. + * @param {string} The user-entered text. + * @return {string} The trimmed query. + */ + SearchPage.canonicalizeQuery = function(text) { + // Trim beginning and ending whitespace. + return text.replace(/^\s+|\s+$/g, ''); + }; + + // Export + return { + SearchPage: SearchPage + }; + +}); diff --git a/chromium/chrome/browser/resources/options/settings_dialog.js b/chromium/chrome/browser/resources/options/settings_dialog.js new file mode 100644 index 00000000000..7391b8695d3 --- /dev/null +++ b/chromium/chrome/browser/resources/options/settings_dialog.js @@ -0,0 +1,70 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * @fileoverview Base class for dialogs that require saving preferences on + * confirm and resetting preference inputs on cancel. + */ + +cr.define('options', function() { + /** @const */ var OptionsPage = options.OptionsPage; + + /** + * Base class for settings dialogs. + * @constructor + * @param {string} name See OptionsPage constructor. + * @param {string} title See OptionsPage constructor. + * @param {string} pageDivName See OptionsPage constructor. + * @param {HTMLInputElement} okButton The confirmation button element. + * @param {HTMLInputElement} cancelButton The cancellation button element. + * @extends {OptionsPage} + */ + function SettingsDialog(name, title, pageDivName, okButton, cancelButton) { + OptionsPage.call(this, name, title, pageDivName); + this.okButton = okButton; + this.cancelButton = cancelButton; + } + + SettingsDialog.prototype = { + __proto__: OptionsPage.prototype, + + /** @override */ + initializePage: function() { + this.okButton.onclick = this.handleConfirm.bind(this); + this.cancelButton.onclick = this.handleCancel.bind(this); + }, + + /** + * Handles the confirm button by saving the dialog preferences. + */ + handleConfirm: function() { + OptionsPage.closeOverlay(); + + var prefs = Preferences.getInstance(); + var els = this.pageDiv.querySelectorAll('[dialog-pref]'); + for (var i = 0; i < els.length; i++) { + if (els[i].pref) + prefs.commitPref(els[i].pref, els[i].metric); + } + }, + + /** + * Handles the cancel button by closing the overlay. + */ + handleCancel: function() { + OptionsPage.closeOverlay(); + + var prefs = Preferences.getInstance(); + var els = this.pageDiv.querySelectorAll('[dialog-pref]'); + for (var i = 0; i < els.length; i++) { + if (els[i].pref) + prefs.rollbackPref(els[i].pref); + } + }, + }; + + return { + SettingsDialog: SettingsDialog + }; +}); diff --git a/chromium/chrome/browser/resources/options/spelling_confirm_overlay.css b/chromium/chrome/browser/resources/options/spelling_confirm_overlay.css new file mode 100644 index 00000000000..434c26c3f0c --- /dev/null +++ b/chromium/chrome/browser/resources/options/spelling_confirm_overlay.css @@ -0,0 +1,8 @@ +/* Copyright (c) 2012 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#spelling-confirm-overlay { + width: 500px; +} diff --git a/chromium/chrome/browser/resources/options/spelling_confirm_overlay.html b/chromium/chrome/browser/resources/options/spelling_confirm_overlay.html new file mode 100644 index 00000000000..fb239994f08 --- /dev/null +++ b/chromium/chrome/browser/resources/options/spelling_confirm_overlay.html @@ -0,0 +1,19 @@ +<div id="spelling-confirm-overlay" class="page" hidden> + <div class="close-button"></div> + <h1 i18n-content="spellingConfirmOverlay"></h1> + <div class="content-area"> + <span id="spelling-confirm-text" i18n-content="spellingConfirmMessage"> + </span> + <a id="spelling-confirm-learn-more" target="_blank" i18n-content="learnMore" + i18n-values="href:privacyLearnMoreURL"></a> + </div> + <div class="action-area"> + <div class="button-strip"> + <button id="spelling-confirm-cancel" i18n-content="spellingConfirmDisable" + class="cancel-button"></button> + <button id="spelling-confirm-ok" class="default-button" + i18n-content="spellingConfirmEnable"> + </button> + </div> + </div> +</div> diff --git a/chromium/chrome/browser/resources/options/startup_overlay.css b/chromium/chrome/browser/resources/options/startup_overlay.css new file mode 100644 index 00000000000..dc74debad7b --- /dev/null +++ b/chromium/chrome/browser/resources/options/startup_overlay.css @@ -0,0 +1,41 @@ +/* Copyright (c) 2012 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ + +#startup-overlay { + min-width: 500px; +} + +#startupPagesList { + margin-bottom: 20px; + min-height: 64px; +} + +#startupPagesList .title { + width: 40%; +} + +#startupPagesList .url { + -webkit-box-flex: 1; + color: #666; +} + +#startupPagesList > * { + max-width: 700px; +} + +#startupPagesListDropmarker { + background-clip: padding-box; + background-color: hsl(214, 91%, 65%); + border: 2px solid hsl(214, 91%, 65%); + border-bottom-color: transparent; + border-radius: 0; + border-top-color: transparent; + box-sizing: border-box; + display: none; + height: 6px; + overflow: hidden; + pointer-events: none; + position: fixed; + z-index: 10; +} diff --git a/chromium/chrome/browser/resources/options/startup_overlay.html b/chromium/chrome/browser/resources/options/startup_overlay.html new file mode 100644 index 00000000000..645bb7ce3bc --- /dev/null +++ b/chromium/chrome/browser/resources/options/startup_overlay.html @@ -0,0 +1,25 @@ +<div id="startup-overlay" class="page" hidden> + <div class="close-button"></div> + <h1 i18n-content="startupPagesOverlay"></h1> + <!-- This <input> element is always hidden. It needs to be here so that + its 'controlled-by' attribute will get set when the urls preference is + managed by a policy, so that the managed prefs bar will show up. + --> + <input type="text" pref="session.startup_urls" hidden> + <div class="content-area"> + <list id="startupPagesList"></list> + </div> + <div class="action-area"> + <span class="hbox stretch"> + <button id="startupUseCurrentButton" + i18n-content="startupUseCurrent"></button> + </span> + <div class="button-strip"> + <button id="startup-overlay-cancel" i18n-content="cancel"></button> + <button id="startup-overlay-confirm" class="default-button" + i18n-content="ok"> + </button> + </div> + </div> + <div id="startupPagesListDropmarker"></div> +</div> diff --git a/chromium/chrome/browser/resources/options/startup_overlay.js b/chromium/chrome/browser/resources/options/startup_overlay.js new file mode 100644 index 00000000000..d63b49d1027 --- /dev/null +++ b/chromium/chrome/browser/resources/options/startup_overlay.js @@ -0,0 +1,175 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + /** @const */ var ArrayDataModel = cr.ui.ArrayDataModel; + /** @const */ var OptionsPage = options.OptionsPage; + /** @const */ var SettingsDialog = options.SettingsDialog; + + /** + * StartupOverlay class + * Encapsulated handling of the 'Set Startup pages' overlay page. + * @constructor + * @class + */ + function StartupOverlay() { + SettingsDialog.call(this, 'startup', + loadTimeData.getString('startupPagesOverlayTabTitle'), + 'startup-overlay', + $('startup-overlay-confirm'), + $('startup-overlay-cancel')); + }; + + cr.addSingletonGetter(StartupOverlay); + + StartupOverlay.prototype = { + __proto__: SettingsDialog.prototype, + + /** + * An autocomplete list that can be attached to a text field during editing. + * @type {HTMLElement} + * @private + */ + autocompleteList_: null, + + startup_pages_pref_: { + 'name': 'session.startup_urls', + 'disabled': false + }, + + /** + * Initialize the page. + */ + initializePage: function() { + SettingsDialog.prototype.initializePage.call(this); + + var self = this; + + var startupPagesList = $('startupPagesList'); + options.browser_options.StartupPageList.decorate(startupPagesList); + startupPagesList.autoExpands = true; + + $('startupUseCurrentButton').onclick = function(event) { + chrome.send('setStartupPagesToCurrentPages'); + }; + + Preferences.getInstance().addEventListener( + this.startup_pages_pref_.name, + this.handleStartupPageListChange_.bind(this)); + + var suggestionList = new cr.ui.AutocompleteList(); + suggestionList.autoExpands = true; + suggestionList.requestSuggestions = + this.requestAutocompleteSuggestions_.bind(this); + $('startup-overlay').appendChild(suggestionList); + this.autocompleteList_ = suggestionList; + startupPagesList.autocompleteList = suggestionList; + }, + + /** @override */ + handleConfirm: function() { + SettingsDialog.prototype.handleConfirm.call(this); + chrome.send('commitStartupPrefChanges'); + // Set the startup behavior to "open specific set of pages" so that the + // pages the user selected actually get opened on startup. + Preferences.setIntegerPref('session.restore_on_startup', 4, true); + }, + + /** @override */ + handleCancel: function() { + SettingsDialog.prototype.handleCancel.call(this); + chrome.send('cancelStartupPrefChanges'); + }, + + /** + * Sets the enabled state of the custom startup page list + * @param {boolean} disable True to disable, false to enable + */ + setControlsDisabled: function(disable) { + var startupPagesList = $('startupPagesList'); + startupPagesList.disabled = disable; + startupPagesList.setAttribute('tabindex', disable ? -1 : 0); + // Explicitly set disabled state for input text elements. + var inputs = startupPagesList.querySelectorAll("input[type='text']"); + for (var i = 0; i < inputs.length; i++) + inputs[i].disabled = disable; + $('startupUseCurrentButton').disabled = disable; + }, + + /** + * Enables or disables the the custom startup page list controls + * based on the whether the 'pages to restore on startup' pref is enabled. + */ + updateControlStates: function() { + this.setControlsDisabled( + this.startup_pages_pref_.disabled); + }, + + /** + * Handles change events of the preference + * 'session.startup_urls'. + * @param {event} preference changed event. + * @private + */ + handleStartupPageListChange_: function(event) { + this.startup_pages_pref_.disabled = event.value.disabled; + this.updateControlStates(); + }, + + /** + * Updates the startup pages list with the given entries. + * @param {Array} pages List of startup pages. + * @private + */ + updateStartupPages_: function(pages) { + var model = new ArrayDataModel(pages); + // Add a "new page" row. + model.push({modelIndex: -1}); + $('startupPagesList').dataModel = model; + }, + + /** + * Sends an asynchronous request for new autocompletion suggestions for the + * the given query. When new suggestions are available, the C++ handler will + * call updateAutocompleteSuggestions_. + * @param {string} query List of autocomplete suggestions. + * @private + */ + requestAutocompleteSuggestions_: function(query) { + chrome.send('requestAutocompleteSuggestionsForStartupPages', [query]); + }, + + /** + * Updates the autocomplete suggestion list with the given entries. + * @param {Array} pages List of autocomplete suggestions. + * @private + */ + updateAutocompleteSuggestions_: function(suggestions) { + var list = this.autocompleteList_; + // If the trigger for this update was a value being selected from the + // current list, do nothing. + if (list.targetInput && list.selectedItem && + list.selectedItem.url == list.targetInput.value) { + return; + } + list.suggestions = suggestions; + }, + }; + + // Forward public APIs to private implementations. + [ + 'updateStartupPages', + 'updateAutocompleteSuggestions', + ].forEach(function(name) { + StartupOverlay[name] = function() { + var instance = StartupOverlay.getInstance(); + return instance[name + '_'].apply(instance, arguments); + }; + }); + + // Export + return { + StartupOverlay: StartupOverlay + }; +}); diff --git a/chromium/chrome/browser/resources/options/startup_section.html b/chromium/chrome/browser/resources/options/startup_section.html new file mode 100644 index 00000000000..2eaea81c0bf --- /dev/null +++ b/chromium/chrome/browser/resources/options/startup_section.html @@ -0,0 +1,53 @@ +<section id="startup-section" guest-visibility="hidden"> + <h3 i18n-content="sectionTitleStartup"></h3> + <div> + <div class="radio"> + <span class="controlled-setting-with-label"> + <input id="startup-newtab" type="radio" name="startup" value="5" + pref="session.restore_on_startup" + metric="Options_Startup_NewTab"> + <span> + <label for="startup-newtab" i18n-content="startupShowNewTab"></label> + <span class="controlled-setting-indicator" + pref="session.restore_on_startup" value="5"> + </span> + </span> + </span> + </div> + <div class="radio"> + <span class="controlled-setting-with-label"> + <input id="startup-restore-session" type="radio" name="startup" + value="1" pref="session.restore_on_startup" + metric="Options_Startup_LastSession"> + <span> + <label for="startup-restore-session" + i18n-content="startupRestoreLastSession"> + </label> + <span class="controlled-setting-indicator" + pref="session.restore_on_startup" value="1"> + </span> + </span> + </span> + </div> + <div class="radio"> + <span class="controlled-setting-with-label"> + <input id="startup-show-pages" type="radio" name="startup" + pref="session.restore_on_startup" + value="4" metric="Options_Startup_Custom"> + <span> + <label for="startup-show-pages" i18n-content="startupShowPages"> + </label> + <span class="controlled-setting-indicator" + pref="session.restore_on_startup" value="4"> + </span> + <button id="startup-set-pages" class="link-button" + i18n-content="startupSetPages"> + </button> + <span class="controlled-setting-indicator" + pref="session.startup_urls"> + </span> + </span> + </label> + </div> + </div> +</section> diff --git a/chromium/chrome/browser/resources/options/subpages_tab_controls.css b/chromium/chrome/browser/resources/options/subpages_tab_controls.css new file mode 100644 index 00000000000..c49c07715fe --- /dev/null +++ b/chromium/chrome/browser/resources/options/subpages_tab_controls.css @@ -0,0 +1,74 @@ +/* Copyright (c) 2012 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. */ + +.subpages-nav-tabs .tab { + padding: 4px 8px; + position: relative; +} + +.subpages-nav-tabs .active-tab { + -webkit-box-shadow: 8px -8px 12px -6px rgb(240, 240, 240); + background: white; + border: 1px solid #ddd; + border-bottom: 2px solid white; + border-top-left-radius: 3px; + border-top-right-radius: 3px; + position: relative; +} + +/* To avoid tabs changing size when they are clicked and their labels become + * bold, we actually put two labels inside each tab: an inactive label and an + * active label. Only one is visible at a time, but the bold label is used to + * size the tab even when it's not visible. This keeps the tab size constant. + */ +.subpages-nav-tabs .active-tab-label, +.subpages-nav-tabs .tab-label:hover { + font-weight: bold; +} + +.subpages-nav-tabs .tab-label { + left: 9px; + position: absolute; + top: 5px; +} + +html[dir=rtl] .subpages-nav-tabs .tab-label { + right: 9px; +} + +.subpages-nav-tabs .active-tab-label, +.subpages-nav-tabs .active-tab .tab-label { + visibility: hidden; +} + +/* .tab is not removed when .active-tab is added, so we must + * override the hidden visibility above in the active tab case. + */ +.subpages-nav-tabs .active-tab .active-tab-label { + visibility: visible; +} + +.subpages-nav-tabs { + background-image: -webkit-gradient( + linear, + left top, + left bottom, + color-stop(0, rgb(255,255,255)), + color-stop(0.6, rgb(255,255,255)), + color-stop(0.8, rgb(250, 250, 250)), + color-stop(1.0, rgb(242,242,242)) + ); + border-bottom: 1px solid #ddd; + padding: 4px 20px; +} + +.subpages-tab-contents { + -webkit-padding-start: 10px; + display: none; + padding-top: 15px; +} + +.active-tab-contents { + display: block; +} diff --git a/chromium/chrome/browser/resources/options/sync_section.html b/chromium/chrome/browser/resources/options/sync_section.html new file mode 100644 index 00000000000..fdf89885921 --- /dev/null +++ b/chromium/chrome/browser/resources/options/sync_section.html @@ -0,0 +1,77 @@ +<if expr="not pp_ifdef('chromeos')"> +<section id="sync-section"> + <h3 i18n-content="sectionTitleSync"></h3> +</if> +<if expr="pp_ifdef('chromeos')"> +<div id="sync-section"> +</if> + + <div id="sync-overview" class="settings-row" hidden> + <p i18n-content="syncOverview"></p> + <a i18n-values="href:syncLearnMoreURL" i18n-content="learnMore" + target="_blank"></a> + </div> + +<if expr="pp_ifdef('chromeos')"> + <div id="account-picture-wrapper"> + <div id="account-picture-control"> + <input type="image" id="account-picture" tabindex="0" + alt="" i18n-values="aria-label:changePicture"> + <div id="change-picture-caption" i18n-content="changePicture"></div> + </div> + <span id="account-picture-indicator" class="controlled-setting-indicator"> + </span> + </div> + <div id="sync-general"> +</if> <!-- pp_ifdef('chromeos') --> + + <div id="sync-status" class="settings-row" hidden> + <span id="sync-status-text"></span> + <button id="sync-action-link" class="link-button"></button> + </div> + +<if expr="pp_ifdef('chromeos')"> + <div class="checkbox"> + <span class="controlled-setting-with-label"> + <input id="enable-screen-lock" type="checkbox" + pref="settings.enable_screen_lock"> + <span> + <label for="enable-screen-lock" i18n-content="enableScreenlock"> + </label> + <span class="controlled-setting-indicator" + pref="settings.enable_screen_lock"> + </span> + </span> + </span> + </div> + </div> +</if> <!-- pp_ifdef('chromeos') --> + + <div id="sync-buttons" class="settings-row"> + <button id="start-stop-sync" hidden></button> + <span id="start-stop-sync-indicator" + class="controlled-setting-indicator" hidden> + </span> + <button id="customize-sync" i18n-content="customizeSync" + pref="sync.managed" hidden> + </button> +<if expr="pp_ifdef('chromeos')"> + <button id="manage-accounts-button" + i18n-content="manageAccountsButtonTitle"> + </button> +</if> <!-- pp_ifdef('chromeos') --> + <div id="enable-auto-login-checkbox" class="checkbox" hidden> + <label> + <input id="enable-auto-login" pref="autologin.enabled" + metric="Options_Autologin" type="checkbox"> + <span i18n-content="autologinEnabled"></span> + </label> + </div> + </div> + +<if expr="not pp_ifdef('chromeos')"> +</section> +</if> +<if expr="pp_ifdef('chromeos')"> +</div> +</if> diff --git a/chromium/chrome/browser/resources/options/yellow_gear.png b/chromium/chrome/browser/resources/options/yellow_gear.png Binary files differnew file mode 100644 index 00000000000..4134d3d2ad9 --- /dev/null +++ b/chromium/chrome/browser/resources/options/yellow_gear.png |