michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: "use strict"; michael@0: michael@0: const {classes: Cc, interfaces: Ci, manager: Cm, utils: Cu} = Components; michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: michael@0: const VKB_ENTER_KEY = 13; // User press of VKB enter key michael@0: const INITIAL_PAGE_DELAY = 500; // Initial pause on program start for scroll alignment michael@0: const PREFS_BUFFER_MAX = 30; // Max prefs buffer size for getPrefsBuffer() michael@0: const PAGE_SCROLL_TRIGGER = 200; // Triggers additional getPrefsBuffer() on user scroll-to-bottom michael@0: const FILTER_CHANGE_TRIGGER = 200; // Delay between responses to filterInput changes michael@0: const INNERHTML_VALUE_DELAY = 100; // Delay before providing prefs innerHTML value michael@0: michael@0: let gStringBundle = Services.strings.createBundle("chrome://browser/locale/config.properties"); michael@0: let gClipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper); michael@0: michael@0: michael@0: /* ============================== NewPrefDialog ============================== michael@0: * michael@0: * New Preference Dialog Object and methods michael@0: * michael@0: * Implements User Interfaces for creation of a single(new) Preference setting michael@0: * michael@0: */ michael@0: var NewPrefDialog = { michael@0: michael@0: _prefsShield: null, michael@0: michael@0: _newPrefsDialog: null, michael@0: _newPrefItem: null, michael@0: _prefNameInputElt: null, michael@0: _prefTypeSelectElt: null, michael@0: michael@0: _booleanValue: null, michael@0: _booleanToggle: null, michael@0: _stringValue: null, michael@0: _intValue: null, michael@0: michael@0: _positiveButton: null, michael@0: michael@0: get type() { michael@0: return this._prefTypeSelectElt.value; michael@0: }, michael@0: michael@0: set type(aType) { michael@0: this._prefTypeSelectElt.value = aType; michael@0: switch(this._prefTypeSelectElt.value) { michael@0: case "boolean": michael@0: this._prefTypeSelectElt.selectedIndex = 0; michael@0: break; michael@0: case "string": michael@0: this._prefTypeSelectElt.selectedIndex = 1; michael@0: break; michael@0: case "int": michael@0: this._prefTypeSelectElt.selectedIndex = 2; michael@0: break; michael@0: } michael@0: michael@0: this._newPrefItem.setAttribute("typestyle", aType); michael@0: }, michael@0: michael@0: // Init the NewPrefDialog michael@0: init: function AC_init() { michael@0: this._prefsShield = document.getElementById("prefs-shield"); michael@0: michael@0: this._newPrefsDialog = document.getElementById("new-pref-container"); michael@0: this._newPrefItem = document.getElementById("new-pref-item"); michael@0: this._prefNameInputElt = document.getElementById("new-pref-name"); michael@0: this._prefTypeSelectElt = document.getElementById("new-pref-type"); michael@0: michael@0: this._booleanValue = document.getElementById("new-pref-value-boolean"); michael@0: this._stringValue = document.getElementById("new-pref-value-string"); michael@0: this._intValue = document.getElementById("new-pref-value-int"); michael@0: michael@0: this._positiveButton = document.getElementById("positive-button"); michael@0: }, michael@0: michael@0: // Called to update positive button to display text ("Create"/"Change), and enabled/disabled status michael@0: // As new pref name is initially displayed, re-focused, or modifed during user input michael@0: _updatePositiveButton: function AC_updatePositiveButton(aPrefName) { michael@0: this._positiveButton.textContent = gStringBundle.GetStringFromName("newPref.createButton"); michael@0: this._positiveButton.setAttribute("disabled", true); michael@0: if (aPrefName == "") { michael@0: return; michael@0: } michael@0: michael@0: // If item already in list, it's being changed, else added michael@0: let item = document.querySelector(".pref-item[name=" + aPrefName.quote() + "]"); michael@0: if (item) { michael@0: this._positiveButton.textContent = gStringBundle.GetStringFromName("newPref.changeButton"); michael@0: } else { michael@0: this._positiveButton.removeAttribute("disabled"); michael@0: } michael@0: }, michael@0: michael@0: // When we want to cancel/hide an existing, or show a new pref dialog michael@0: toggleShowHide: function AC_toggleShowHide() { michael@0: if (this._newPrefsDialog.classList.contains("show")) { michael@0: this.hide(); michael@0: } else { michael@0: this._show(); michael@0: } michael@0: }, michael@0: michael@0: // When we want to show the new pref dialog / shield the prefs list michael@0: _show: function AC_show() { michael@0: this._newPrefsDialog.classList.add("show"); michael@0: this._prefsShield.setAttribute("shown", true); michael@0: michael@0: // Initial default field values michael@0: this._prefNameInputElt.value = ""; michael@0: this._updatePositiveButton(this._prefNameInputElt.value); michael@0: michael@0: this.type = "boolean"; michael@0: this._booleanValue.value = "false"; michael@0: this._stringValue.value = ""; michael@0: this._intValue.value = ""; michael@0: michael@0: this._prefNameInputElt.focus(); michael@0: michael@0: window.addEventListener("keypress", this.handleKeypress, false); michael@0: }, michael@0: michael@0: // When we want to cancel/hide the new pref dialog / un-shield the prefs list michael@0: hide: function AC_hide() { michael@0: this._newPrefsDialog.classList.remove("show"); michael@0: this._prefsShield.removeAttribute("shown"); michael@0: michael@0: window.removeEventListener("keypress", this.handleKeypress, false); michael@0: }, michael@0: michael@0: // Watch user key input so we can provide Enter key action, commit input values michael@0: handleKeypress: function AC_handleKeypress(aEvent) { michael@0: // Close our VKB on new pref enter key press michael@0: if (aEvent.keyCode == VKB_ENTER_KEY) michael@0: aEvent.target.blur(); michael@0: }, michael@0: michael@0: // New prefs create dialog only allows creating a non-existing preference, doesn't allow for michael@0: // Changing an existing one on-the-fly, tap existing/displayed line item pref for that michael@0: create: function AC_create(aEvent) { michael@0: if (this._positiveButton.getAttribute("disabled") == "true") { michael@0: return; michael@0: } michael@0: michael@0: switch(this.type) { michael@0: case "boolean": michael@0: Services.prefs.setBoolPref(this._prefNameInputElt.value, (this._booleanValue.value == "true") ? true : false); michael@0: break; michael@0: case "string": michael@0: Services.prefs.setCharPref(this._prefNameInputElt.value, this._stringValue.value); michael@0: break; michael@0: case "int": michael@0: Services.prefs.setIntPref(this._prefNameInputElt.value, this._intValue.value); michael@0: break; michael@0: } michael@0: michael@0: this.hide(); michael@0: }, michael@0: michael@0: // Display proper positive button text/state on new prefs name input focus michael@0: focusName: function AC_focusName(aEvent) { michael@0: this._updatePositiveButton(aEvent.target.value); michael@0: }, michael@0: michael@0: // Display proper positive button text/state as user changes new prefs name michael@0: updateName: function AC_updateName(aEvent) { michael@0: this._updatePositiveButton(aEvent.target.value); michael@0: }, michael@0: michael@0: // In new prefs dialog, bool prefs are , as they aren't yet tied to an michael@0: // Actual Services.prefs.*etBoolPref() michael@0: toggleBoolValue: function AC_toggleBoolValue() { michael@0: this._booleanValue.value = (this._booleanValue.value == "true" ? "false" : "true"); michael@0: } michael@0: } michael@0: michael@0: michael@0: /* ============================== AboutConfig ============================== michael@0: * michael@0: * Main AboutConfig object and methods michael@0: * michael@0: * Implements User Interfaces for maintenance of a list of Preference settings michael@0: * michael@0: */ michael@0: var AboutConfig = { michael@0: michael@0: contextMenuLINode: null, michael@0: filterInput: null, michael@0: _filterPrevInput: null, michael@0: _filterChangeTimer: null, michael@0: _prefsContainer: null, michael@0: _loadingContainer: null, michael@0: _list: null, michael@0: michael@0: // Init the main AboutConfig dialog michael@0: init: function AC_init() { michael@0: this.filterInput = document.getElementById("filter-input"); michael@0: this._prefsContainer = document.getElementById("prefs-container"); michael@0: this._loadingContainer = document.getElementById("loading-container"); michael@0: michael@0: let list = Services.prefs.getChildList(""); michael@0: this._list = list.sort().map( function AC_getMapPref(aPref) { michael@0: return new Pref(aPref); michael@0: }, this); michael@0: michael@0: // Display the current prefs list (retains searchFilter value) michael@0: this.bufferFilterInput(); michael@0: michael@0: // Setup the prefs observers michael@0: Services.prefs.addObserver("", this, false); michael@0: }, michael@0: michael@0: // Uninit the main AboutConfig dialog michael@0: uninit: function AC_uninit() { michael@0: // Remove the prefs observer michael@0: Services.prefs.removeObserver("", this); michael@0: michael@0: // Ensure pref adds/changes/resets flushed to disk on unload michael@0: Services.prefs.savePrefFile(null); michael@0: }, michael@0: michael@0: // Clear the filterInput value, to display the entire list michael@0: clearFilterInput: function AC_clearFilterInput() { michael@0: this.filterInput.value = ""; michael@0: this.bufferFilterInput(); michael@0: }, michael@0: michael@0: // Buffer down rapid changes in filterInput value from keyboard michael@0: bufferFilterInput: function AC_bufferFilterInput() { michael@0: if (this._filterChangeTimer) { michael@0: clearTimeout(this._filterChangeTimer); michael@0: } michael@0: michael@0: this._filterChangeTimer = setTimeout((function() { michael@0: this._filterChangeTimer = null; michael@0: // Display updated prefs list when filterInput value settles michael@0: this._displayNewList(); michael@0: }).bind(this), FILTER_CHANGE_TRIGGER); michael@0: }, michael@0: michael@0: // Update displayed list when filterInput value changes michael@0: _displayNewList: function AC_displayNewList() { michael@0: // This survives the search filter value past a page refresh michael@0: this.filterInput.setAttribute("value", this.filterInput.value); michael@0: michael@0: // Don't start new filter search if same as last michael@0: if (this.filterInput.value == this._filterPrevInput) { michael@0: return; michael@0: } michael@0: this._filterPrevInput = this.filterInput.value; michael@0: michael@0: // Clear list item selection / context menu, prefs list, get first buffer, set scrolling on michael@0: this.selected = ""; michael@0: this._clearPrefsContainer(); michael@0: this._addMorePrefsToContainer(); michael@0: window.onscroll = this.onScroll.bind(this); michael@0: michael@0: // Pause for screen to settle, then ensure at top michael@0: setTimeout((function() { michael@0: window.scrollTo(0, 0); michael@0: }).bind(this), INITIAL_PAGE_DELAY); michael@0: }, michael@0: michael@0: // Clear the displayed preferences list michael@0: _clearPrefsContainer: function AC_clearPrefsContainer() { michael@0: // Quick clear the prefsContainer list michael@0: let empty = this._prefsContainer.cloneNode(false); michael@0: this._prefsContainer.parentNode.replaceChild(empty, this._prefsContainer); michael@0: this._prefsContainer = empty; michael@0: michael@0: // Quick clear the prefs li.HTML list michael@0: this._list.forEach(function(item) { michael@0: delete item.li; michael@0: }); michael@0: }, michael@0: michael@0: // Get a small manageable block of prefs items, and add them to the displayed list michael@0: _addMorePrefsToContainer: function AC_addMorePrefsToContainer() { michael@0: // Create filter regex michael@0: let filterExp = this.filterInput.value ? michael@0: new RegExp(this.filterInput.value, "i") : null; michael@0: michael@0: // Get a new block for the display list michael@0: let prefsBuffer = []; michael@0: for (let i = 0; i < this._list.length && prefsBuffer.length < PREFS_BUFFER_MAX; i++) { michael@0: if (!this._list[i].li && this._list[i].test(filterExp)) { michael@0: prefsBuffer.push(this._list[i]); michael@0: } michael@0: } michael@0: michael@0: // Add the new block to the displayed list michael@0: for (let i = 0; i < prefsBuffer.length; i++) { michael@0: this._prefsContainer.appendChild(prefsBuffer[i].getOrCreateNewLINode()); michael@0: } michael@0: michael@0: // Determine if anything left to add later by scrolling michael@0: let anotherPrefsBufferRemains = false; michael@0: for (let i = 0; i < this._list.length; i++) { michael@0: if (!this._list[i].li && this._list[i].test(filterExp)) { michael@0: anotherPrefsBufferRemains = true; michael@0: break; michael@0: } michael@0: } michael@0: michael@0: if (anotherPrefsBufferRemains) { michael@0: // If still more could be displayed, show the throbber michael@0: this._loadingContainer.style.display = "block"; michael@0: } else { michael@0: // If no more could be displayed, hide the throbber, and stop noticing scroll events michael@0: this._loadingContainer.style.display = "none"; michael@0: window.onscroll = null; michael@0: } michael@0: }, michael@0: michael@0: // If scrolling at the bottom, maybe add some more entries michael@0: onScroll: function AC_onScroll(aEvent) { michael@0: if (this._prefsContainer.scrollHeight - (window.pageYOffset + window.innerHeight) < PAGE_SCROLL_TRIGGER) { michael@0: if (!this._filterChangeTimer) { michael@0: this._addMorePrefsToContainer(); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: michael@0: // Return currently selected list item node michael@0: get selected() { michael@0: return document.querySelector(".pref-item.selected"); michael@0: }, michael@0: michael@0: // Set list item node as selected michael@0: set selected(aSelection) { michael@0: let currentSelection = this.selected; michael@0: if (aSelection == currentSelection) { michael@0: return; michael@0: } michael@0: michael@0: // Clear any previous selection michael@0: if (currentSelection) { michael@0: currentSelection.classList.remove("selected"); michael@0: currentSelection.removeEventListener("keypress", this.handleKeypress, false); michael@0: } michael@0: michael@0: // Set any current selection michael@0: if (aSelection) { michael@0: aSelection.classList.add("selected"); michael@0: aSelection.addEventListener("keypress", this.handleKeypress, false); michael@0: } michael@0: }, michael@0: michael@0: // Watch user key input so we can provide Enter key action, commit input values michael@0: handleKeypress: function AC_handleKeypress(aEvent) { michael@0: if (aEvent.keyCode == VKB_ENTER_KEY) michael@0: aEvent.target.blur(); michael@0: }, michael@0: michael@0: // Return the target list item node of an action event michael@0: getLINodeForEvent: function AC_getLINodeForEvent(aEvent) { michael@0: let node = aEvent.target; michael@0: while (node && node.nodeName != "li") { michael@0: node = node.parentNode; michael@0: } michael@0: michael@0: return node; michael@0: }, michael@0: michael@0: // Return a pref of a list item node michael@0: _getPrefForNode: function AC_getPrefForNode(aNode) { michael@0: let pref = aNode.getAttribute("name"); michael@0: michael@0: return new Pref(pref); michael@0: }, michael@0: michael@0: // When list item name or value are tapped michael@0: selectOrToggleBoolPref: function AC_selectOrToggleBoolPref(aEvent) { michael@0: let node = this.getLINodeForEvent(aEvent); michael@0: michael@0: // If not already selected, just do so michael@0: if (this.selected != node) { michael@0: this.selected = node; michael@0: return; michael@0: } michael@0: michael@0: // If already selected, and value is boolean, toggle it michael@0: let pref = this._getPrefForNode(node); michael@0: if (pref.type != Services.prefs.PREF_BOOL) { michael@0: return; michael@0: } michael@0: michael@0: this.toggleBoolPref(aEvent); michael@0: }, michael@0: michael@0: // When finalizing list input values due to blur michael@0: setIntOrStringPref: function AC_setIntOrStringPref(aEvent) { michael@0: let node = this.getLINodeForEvent(aEvent); michael@0: michael@0: // Skip if locked michael@0: let pref = this._getPrefForNode(node); michael@0: if (pref.locked) { michael@0: return; michael@0: } michael@0: michael@0: // Boolean inputs blur to remove focus from "button" michael@0: if (pref.type == Services.prefs.PREF_BOOL) { michael@0: return; michael@0: } michael@0: michael@0: // String and Int inputs change / commit on blur michael@0: pref.value = aEvent.target.value; michael@0: }, michael@0: michael@0: // When we reset a pref to it's default value (note resetting a user created pref will delete it) michael@0: resetDefaultPref: function AC_resetDefaultPref(aEvent) { michael@0: let node = this.getLINodeForEvent(aEvent); michael@0: michael@0: // If not already selected, do so michael@0: if (this.selected != node) { michael@0: this.selected = node; michael@0: } michael@0: michael@0: // Reset will handle any locked condition michael@0: let pref = this._getPrefForNode(node); michael@0: pref.reset(); michael@0: }, michael@0: michael@0: // When we want to toggle a bool pref michael@0: toggleBoolPref: function AC_toggleBoolPref(aEvent) { michael@0: let node = this.getLINodeForEvent(aEvent); michael@0: michael@0: // Skip if locked, or not boolean michael@0: let pref = this._getPrefForNode(node); michael@0: if (pref.locked) { michael@0: return; michael@0: } michael@0: michael@0: // Toggle, and blur to remove field focus michael@0: pref.value = !pref.value; michael@0: aEvent.target.blur(); michael@0: }, michael@0: michael@0: // When Int inputs have their Up or Down arrows toggled michael@0: incrOrDecrIntPref: function AC_incrOrDecrIntPref(aEvent, aInt) { michael@0: let node = this.getLINodeForEvent(aEvent); michael@0: michael@0: // Skip if locked michael@0: let pref = this._getPrefForNode(node); michael@0: if (pref.locked) { michael@0: return; michael@0: } michael@0: michael@0: pref.value += aInt; michael@0: }, michael@0: michael@0: // Observe preference changes michael@0: observe: function AC_observe(aSubject, aTopic, aPrefName) { michael@0: let pref = new Pref(aPrefName); michael@0: michael@0: // Ignore uninteresting changes, and avoid "private" preferences michael@0: if (aTopic != "nsPref:changed") { michael@0: return; michael@0: } michael@0: michael@0: // If pref type invalid, refresh display as user reset/removed an item from the list michael@0: if (pref.type == Services.prefs.PREF_INVALID) { michael@0: document.location.reload(); michael@0: return; michael@0: } michael@0: michael@0: // If pref not already in list, refresh display as it's being added michael@0: let item = document.querySelector(".pref-item[name=" + pref.name.quote() + "]"); michael@0: if (!item) { michael@0: document.location.reload(); michael@0: return; michael@0: } michael@0: michael@0: // Else we're modifying a pref michael@0: item.setAttribute("value", pref.value); michael@0: let input = item.querySelector("input"); michael@0: input.setAttribute("value", pref.value); michael@0: input.value = pref.value; michael@0: michael@0: pref.default ? michael@0: item.querySelector(".reset").setAttribute("disabled", "true") : michael@0: item.querySelector(".reset").removeAttribute("disabled"); michael@0: }, michael@0: michael@0: // Quick context menu helpers for about:config michael@0: clipboardCopy: function AC_clipboardCopy(aField) { michael@0: let pref = this._getPrefForNode(this.contextMenuLINode); michael@0: if (aField == 'name') { michael@0: gClipboardHelper.copyString(pref.name); michael@0: } else { michael@0: gClipboardHelper.copyString(pref.value); michael@0: } michael@0: } michael@0: } michael@0: michael@0: michael@0: /* ============================== Pref ============================== michael@0: * michael@0: * Individual Preference object / methods michael@0: * michael@0: * Defines a Pref object, a document list item tied to Preferences Services michael@0: * And the methods by which they interact. michael@0: * michael@0: */ michael@0: function Pref(aName) { michael@0: this.name = aName; michael@0: } michael@0: michael@0: Pref.prototype = { michael@0: get type() { michael@0: return Services.prefs.getPrefType(this.name); michael@0: }, michael@0: michael@0: get value() { michael@0: switch (this.type) { michael@0: case Services.prefs.PREF_BOOL: michael@0: return Services.prefs.getBoolPref(this.name); michael@0: case Services.prefs.PREF_INT: michael@0: return Services.prefs.getIntPref(this.name); michael@0: case Services.prefs.PREF_STRING: michael@0: default: michael@0: return Services.prefs.getCharPref(this.name); michael@0: } michael@0: michael@0: }, michael@0: set value(aPrefValue) { michael@0: switch (this.type) { michael@0: case Services.prefs.PREF_BOOL: michael@0: Services.prefs.setBoolPref(this.name, aPrefValue); michael@0: break; michael@0: case Services.prefs.PREF_INT: michael@0: Services.prefs.setIntPref(this.name, aPrefValue); michael@0: break; michael@0: case Services.prefs.PREF_STRING: michael@0: default: michael@0: Services.prefs.setCharPref(this.name, aPrefValue); michael@0: } michael@0: }, michael@0: michael@0: get default() { michael@0: return !Services.prefs.prefHasUserValue(this.name); michael@0: }, michael@0: michael@0: get locked() { michael@0: return Services.prefs.prefIsLocked(this.name); michael@0: }, michael@0: michael@0: reset: function AC_reset() { michael@0: Services.prefs.clearUserPref(this.name); michael@0: }, michael@0: michael@0: test: function AC_test(aValue) { michael@0: return aValue ? aValue.test(this.name) : true; michael@0: }, michael@0: michael@0: // Get existing or create new LI node for the pref michael@0: getOrCreateNewLINode: function AC_getOrCreateNewLINode() { michael@0: if (!this.li) { michael@0: this.li = document.createElement("li"); michael@0: michael@0: this.li.className = "pref-item"; michael@0: this.li.setAttribute("name", this.name); michael@0: michael@0: // Click callback to ensure list item selected even on no-action tap events michael@0: this.li.addEventListener("click", michael@0: function(aEvent) { michael@0: AboutConfig.selected = AboutConfig.getLINodeForEvent(aEvent); michael@0: }, michael@0: false michael@0: ); michael@0: michael@0: // Contextmenu callback to identify selected list item michael@0: this.li.addEventListener("contextmenu", michael@0: function(aEvent) { michael@0: AboutConfig.contextMenuLINode = AboutConfig.getLINodeForEvent(aEvent); michael@0: }, michael@0: false michael@0: ); michael@0: michael@0: this.li.setAttribute("contextmenu", "prefs-context-menu"); michael@0: michael@0: // Create list item outline, bind to object actions michael@0: this.li.innerHTML = michael@0: "