Home | History | Annotate | Download | only in settings
      1 /*
      2  * Copyright (C) 2013 Google Inc. All rights reserved.
      3  *
      4  * Redistribution and use in source and binary forms, with or without
      5  * modification, are permitted provided that the following conditions are
      6  * met:
      7  *
      8  *     * Redistributions of source code must retain the above copyright
      9  * notice, this list of conditions and the following disclaimer.
     10  *     * Redistributions in binary form must reproduce the above
     11  * copyright notice, this list of conditions and the following disclaimer
     12  * in the documentation and/or other materials provided with the
     13  * distribution.
     14  *     * Neither the name of Google Inc. nor the names of its
     15  * contributors may be used to endorse or promote products derived from
     16  * this software without specific prior written permission.
     17  *
     18  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     19  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
     20  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
     21  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
     22  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     23  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
     24  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
     25  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
     26  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     27  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     28  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     29  */
     30 
     31 /**
     32  * @constructor
     33  * @param {!function()} onHide
     34  * @extends {WebInspector.HelpScreen}
     35  */
     36 WebInspector.SettingsScreen = function(onHide)
     37 {
     38     WebInspector.HelpScreen.call(this);
     39     this.element.id = "settings-screen";
     40 
     41     /** @type {function()} */
     42     this._onHide = onHide;
     43 
     44     this._tabbedPane = new WebInspector.TabbedPane();
     45     this._tabbedPane.element.classList.add("help-window-main");
     46     var settingsLabelElement = document.createElementWithClass("div", "help-window-label");
     47     settingsLabelElement.createTextChild(WebInspector.UIString("Settings"));
     48     this._tabbedPane.element.insertBefore(settingsLabelElement, this._tabbedPane.element.firstChild);
     49     this._tabbedPane.element.appendChild(this._createCloseButton());
     50     this._tabbedPane.appendTab(WebInspector.SettingsScreen.Tabs.General, WebInspector.UIString("General"), new WebInspector.GenericSettingsTab());
     51     this._tabbedPane.appendTab(WebInspector.SettingsScreen.Tabs.Workspace, WebInspector.UIString("Workspace"), new WebInspector.WorkspaceSettingsTab());
     52     if (Runtime.experiments.supportEnabled())
     53         this._tabbedPane.appendTab(WebInspector.SettingsScreen.Tabs.Experiments, WebInspector.UIString("Experiments"), new WebInspector.ExperimentsSettingsTab());
     54     this._tabbedPane.appendTab(WebInspector.SettingsScreen.Tabs.Shortcuts, WebInspector.UIString("Shortcuts"), WebInspector.shortcutsScreen.createShortcutsTabView());
     55     this._tabbedPane.shrinkableTabs = false;
     56     this._tabbedPane.verticalTabLayout = true;
     57 
     58     this._lastSelectedTabSetting = WebInspector.settings.createSetting("lastSelectedSettingsTab", WebInspector.SettingsScreen.Tabs.General);
     59     this.selectTab(this._lastSelectedTabSetting.get());
     60     this._tabbedPane.addEventListener(WebInspector.TabbedPane.EventTypes.TabSelected, this._tabSelected, this);
     61     this.element.addEventListener("keydown", this._keyDown.bind(this), false);
     62     this._developerModeCounter = 0;
     63 }
     64 
     65 /**
     66  * @param {number} min
     67  * @param {number} max
     68  * @param {string} text
     69  * @return {?string}
     70  */
     71 WebInspector.SettingsScreen.integerValidator = function(min, max, text)
     72 {
     73     var value = Number(text);
     74     if (isNaN(value))
     75         return WebInspector.UIString("Invalid number format");
     76     if (value < min || value > max)
     77         return WebInspector.UIString("Value is out of range [%d, %d]", min, max);
     78     return null;
     79 }
     80 
     81 WebInspector.SettingsScreen.Tabs = {
     82     General: "general",
     83     Overrides: "overrides",
     84     Workspace: "workspace",
     85     Experiments: "experiments",
     86     Shortcuts: "shortcuts"
     87 }
     88 
     89 WebInspector.SettingsScreen.prototype = {
     90     /**
     91      * @param {string} tabId
     92      */
     93     selectTab: function(tabId)
     94     {
     95         this._tabbedPane.selectTab(tabId);
     96     },
     97 
     98     /**
     99      * @param {!WebInspector.Event} event
    100      */
    101     _tabSelected: function(event)
    102     {
    103         this._lastSelectedTabSetting.set(this._tabbedPane.selectedTabId);
    104     },
    105 
    106     /**
    107      * @override
    108      */
    109     wasShown: function()
    110     {
    111         this._tabbedPane.show(this.element);
    112         WebInspector.HelpScreen.prototype.wasShown.call(this);
    113     },
    114 
    115     /**
    116      * @override
    117      * @return {boolean}
    118      */
    119     isClosingKey: function(keyCode)
    120     {
    121         return [
    122             WebInspector.KeyboardShortcut.Keys.Enter.code,
    123             WebInspector.KeyboardShortcut.Keys.Esc.code,
    124         ].indexOf(keyCode) >= 0;
    125     },
    126 
    127     /**
    128      * @override
    129      */
    130     willHide: function()
    131     {
    132         this._onHide();
    133         WebInspector.HelpScreen.prototype.willHide.call(this);
    134     },
    135 
    136     /**
    137      * @param {!Event} event
    138      */
    139     _keyDown: function(event)
    140     {
    141         var shiftKeyCode = 16;
    142         if (event.keyCode === shiftKeyCode && ++this._developerModeCounter > 5)
    143             this.element.classList.add("settings-developer-mode");
    144     },
    145 
    146     __proto__: WebInspector.HelpScreen.prototype
    147 }
    148 
    149 /**
    150  * @constructor
    151  * @extends {WebInspector.VBox}
    152  * @param {string} name
    153  * @param {string=} id
    154  */
    155 WebInspector.SettingsTab = function(name, id)
    156 {
    157     WebInspector.VBox.call(this);
    158     this.element.classList.add("settings-tab-container");
    159     if (id)
    160         this.element.id = id;
    161     var header = this.element.createChild("header");
    162     header.createChild("h3").createTextChild(name);
    163     this.containerElement = this.element.createChild("div", "help-container-wrapper").createChild("div", "settings-tab help-content help-container");
    164 }
    165 
    166 WebInspector.SettingsTab.prototype = {
    167     /**
    168      *  @param {string=} name
    169      *  @return {!Element}
    170      */
    171     _appendSection: function(name)
    172     {
    173         var block = this.containerElement.createChild("div", "help-block");
    174         if (name)
    175             block.createChild("div", "help-section-title").textContent = name;
    176         return block;
    177     },
    178 
    179     _createSelectSetting: function(name, options, setting)
    180     {
    181         var p = document.createElement("p");
    182         p.createChild("label").textContent = name;
    183 
    184         var select = p.createChild("select", "chrome-select");
    185         var settingValue = setting.get();
    186 
    187         for (var i = 0; i < options.length; ++i) {
    188             var option = options[i];
    189             select.add(new Option(option[0], option[1]));
    190             if (settingValue === option[1])
    191                 select.selectedIndex = i;
    192         }
    193 
    194         function changeListener(e)
    195         {
    196             // Don't use e.target.value to avoid conversion of the value to string.
    197             setting.set(options[select.selectedIndex][1]);
    198         }
    199 
    200         select.addEventListener("change", changeListener, false);
    201         return p;
    202     },
    203 
    204     __proto__: WebInspector.VBox.prototype
    205 }
    206 
    207 /**
    208  * @constructor
    209  * @extends {WebInspector.SettingsTab}
    210  */
    211 WebInspector.GenericSettingsTab = function()
    212 {
    213     WebInspector.SettingsTab.call(this, WebInspector.UIString("General"), "general-tab-content");
    214 
    215     this._populateSectionsFromExtensions();
    216 
    217     var restoreDefaults = this._appendSection().createChild("input", "text-button");
    218     restoreDefaults.type = "button";
    219     restoreDefaults.value = WebInspector.UIString("Restore defaults and reload");
    220     restoreDefaults.addEventListener("click", restoreAndReload, false);
    221 
    222     function restoreAndReload()
    223     {
    224         if (window.localStorage)
    225             window.localStorage.clear();
    226         WebInspector.reload();
    227     }
    228 }
    229 
    230 WebInspector.GenericSettingsTab.prototype = {
    231     _populateSectionsFromExtensions: function()
    232     {
    233         /** @const */
    234         var explicitSectionOrder = ["", "Appearance", "Elements", "Sources", "Profiler", "Console", "Extensions"];
    235 
    236         var allExtensions = self.runtime.extensions("ui-setting");
    237 
    238         /** @type {!StringMultimap.<!Runtime.Extension>} */
    239         var extensionsBySectionId = new StringMultimap();
    240         /** @type {!StringMultimap.<!Runtime.Extension>} */
    241         var childSettingExtensionsByParentName = new StringMultimap();
    242 
    243         allExtensions.forEach(function(extension) {
    244             var descriptor = extension.descriptor();
    245             var sectionName = descriptor["section"] || "";
    246             if (!sectionName && descriptor["parentSettingName"]) {
    247                 childSettingExtensionsByParentName.set(descriptor["parentSettingName"], extension);
    248                 return;
    249             }
    250             extensionsBySectionId.set(sectionName, extension);
    251         });
    252 
    253         var sectionIds = extensionsBySectionId.keys();
    254         var explicitlyOrderedSections = explicitSectionOrder.keySet();
    255         for (var i = 0; i < explicitSectionOrder.length; ++i) {
    256             var extensions = extensionsBySectionId.get(explicitSectionOrder[i]);
    257             if (!extensions.size())
    258                 continue;
    259             this._addSectionWithExtensionProvidedSettings(explicitSectionOrder[i], extensions.values(), childSettingExtensionsByParentName);
    260         }
    261         for (var i = 0; i < sectionIds.length; ++i) {
    262             if (explicitlyOrderedSections[sectionIds[i]])
    263                 continue;
    264             this._addSectionWithExtensionProvidedSettings(sectionIds[i], extensionsBySectionId.get(sectionIds[i]).values(), childSettingExtensionsByParentName);
    265         }
    266     },
    267 
    268     /**
    269      * @param {string} sectionName
    270      * @param {!Array.<!Runtime.Extension>} extensions
    271      * @param {!StringMultimap.<!Runtime.Extension>} childSettingExtensionsByParentName
    272      */
    273     _addSectionWithExtensionProvidedSettings: function(sectionName, extensions, childSettingExtensionsByParentName)
    274     {
    275         var uiSectionName = sectionName && WebInspector.UIString(sectionName);
    276         var sectionElement = this._appendSection(uiSectionName);
    277         extensions.forEach(processSetting.bind(this, null));
    278 
    279         /**
    280          * @param {?Element} parentFieldset
    281          * @param {!Runtime.Extension} extension
    282          * @this {WebInspector.GenericSettingsTab}
    283          */
    284         function processSetting(parentFieldset, extension)
    285         {
    286             var descriptor = extension.descriptor();
    287             var experimentName = descriptor["experiment"];
    288             if (experimentName && !Runtime.experiments.isEnabled(experimentName))
    289                 return;
    290 
    291             var settingName = descriptor["settingName"];
    292             var setting = WebInspector.settings[settingName];
    293             var instance = extension.instance();
    294             var settingControl;
    295             if (instance && descriptor["settingType"] === "custom") {
    296                 settingControl = instance.settingElement();
    297                 if (!settingControl)
    298                     return;
    299             }
    300             if (!settingControl) {
    301                 var uiTitle = WebInspector.UIString(descriptor["title"]);
    302                 settingControl = createSettingControl.call(this, uiTitle, setting, descriptor, instance);
    303             }
    304             if (settingName) {
    305                 var childSettings = childSettingExtensionsByParentName.get(settingName);
    306                 if (childSettings.size()) {
    307                     var fieldSet = WebInspector.SettingsUI.createSettingFieldset(setting);
    308                     settingControl.appendChild(fieldSet);
    309                     childSettings.values().forEach(function(item) { processSetting.call(this, fieldSet, item); }, this);
    310                 }
    311             }
    312             var containerElement = parentFieldset || sectionElement;
    313             containerElement.appendChild(settingControl);
    314         }
    315 
    316         /**
    317          * @param {string} uiTitle
    318          * @param {!WebInspector.Setting} setting
    319          * @param {!Object} descriptor
    320          * @param {?Object} instance
    321          * @return {!Element}
    322          * @this {WebInspector.GenericSettingsTab}
    323          */
    324         function createSettingControl(uiTitle, setting, descriptor, instance)
    325         {
    326             switch (descriptor["settingType"]) {
    327             case "checkbox":
    328                 return WebInspector.SettingsUI.createSettingCheckbox(uiTitle, setting);
    329             case "select":
    330                 var descriptorOptions = descriptor["options"]
    331                 var options = new Array(descriptorOptions.length);
    332                 for (var i = 0; i < options.length; ++i) {
    333                     // The third array item flags that the option name is "raw" (non-i18n-izable).
    334                     var optionName = descriptorOptions[i][2] ? descriptorOptions[i][0] : WebInspector.UIString(descriptorOptions[i][0]);
    335                     options[i] = [WebInspector.UIString(descriptorOptions[i][0]), descriptorOptions[i][1]];
    336                 }
    337                 return this._createSelectSetting(uiTitle, options, setting);
    338             default:
    339                 throw "Invalid setting type: " + descriptor["settingType"];
    340             }
    341         }
    342     },
    343 
    344     __proto__: WebInspector.SettingsTab.prototype
    345 }
    346 
    347 /**
    348  * @constructor
    349  * @extends {WebInspector.UISettingDelegate}
    350  */
    351 WebInspector.SettingsScreen.SkipStackFramePatternSettingDelegate = function()
    352 {
    353     WebInspector.UISettingDelegate.call(this);
    354 }
    355 
    356 WebInspector.SettingsScreen.SkipStackFramePatternSettingDelegate.prototype = {
    357     /**
    358      * @override
    359      * @return {!Element}
    360      */
    361     settingElement: function()
    362     {
    363         var button = document.createElementWithClass("input", "text-button");
    364         button.type = "button";
    365         button.value = WebInspector.manageBlackboxingButtonLabel();
    366         button.title = WebInspector.UIString("Skip stepping through sources with particular names");
    367         button.addEventListener("click", this._onManageButtonClick.bind(this), false);
    368         return button;
    369     },
    370 
    371     _onManageButtonClick: function()
    372     {
    373         WebInspector.FrameworkBlackboxDialog.show(WebInspector.inspectorView.element);
    374     },
    375 
    376     __proto__: WebInspector.UISettingDelegate.prototype
    377 }
    378 
    379 /**
    380  * @constructor
    381  * @extends {WebInspector.SettingsTab}
    382  */
    383 WebInspector.WorkspaceSettingsTab = function()
    384 {
    385     WebInspector.SettingsTab.call(this, WebInspector.UIString("Workspace"), "workspace-tab-content");
    386     WebInspector.isolatedFileSystemManager.addEventListener(WebInspector.IsolatedFileSystemManager.Events.FileSystemAdded, this._fileSystemAdded, this);
    387     WebInspector.isolatedFileSystemManager.addEventListener(WebInspector.IsolatedFileSystemManager.Events.FileSystemRemoved, this._fileSystemRemoved, this);
    388 
    389     this._commonSection = this._appendSection(WebInspector.UIString("Common"));
    390     var folderExcludePatternInput = WebInspector.SettingsUI.createSettingInputField(WebInspector.UIString("Folder exclude pattern"), WebInspector.settings.workspaceFolderExcludePattern, false, 0, "270px", WebInspector.SettingsUI.regexValidator);
    391     this._commonSection.appendChild(folderExcludePatternInput);
    392 
    393     this._fileSystemsSection = this._appendSection(WebInspector.UIString("Folders"));
    394     this._fileSystemsListContainer = this._fileSystemsSection.createChild("p", "settings-list-container");
    395 
    396     this._addFileSystemRowElement = this._fileSystemsSection.createChild("div");
    397     var addFileSystemButton = this._addFileSystemRowElement.createChild("input", "text-button");
    398     addFileSystemButton.type = "button";
    399     addFileSystemButton.value = WebInspector.UIString("Add folder\u2026");
    400     addFileSystemButton.addEventListener("click", this._addFileSystemClicked.bind(this), false);
    401 
    402     this._editFileSystemButton = this._addFileSystemRowElement.createChild("input", "text-button");
    403     this._editFileSystemButton.type = "button";
    404     this._editFileSystemButton.value = WebInspector.UIString("Folder options\u2026");
    405     this._editFileSystemButton.addEventListener("click", this._editFileSystemClicked.bind(this), false);
    406     this._updateEditFileSystemButtonState();
    407 
    408     this._reset();
    409 }
    410 
    411 WebInspector.WorkspaceSettingsTab.prototype = {
    412     wasShown: function()
    413     {
    414         WebInspector.SettingsTab.prototype.wasShown.call(this);
    415         this._reset();
    416     },
    417 
    418     _reset: function()
    419     {
    420         this._resetFileSystems();
    421     },
    422 
    423     _resetFileSystems: function()
    424     {
    425         this._fileSystemsListContainer.removeChildren();
    426         var fileSystemPaths = WebInspector.isolatedFileSystemManager.mapping().fileSystemPaths();
    427         delete this._fileSystemsList;
    428 
    429         if (!fileSystemPaths.length) {
    430             var noFileSystemsMessageElement = this._fileSystemsListContainer.createChild("div", "no-file-systems-message");
    431             noFileSystemsMessageElement.textContent = WebInspector.UIString("You have no file systems added.");
    432             return;
    433         }
    434 
    435         this._fileSystemsList = new WebInspector.SettingsList([{ id: "path" }], this._renderFileSystem.bind(this));
    436         this._fileSystemsList.element.classList.add("file-systems-list");
    437         this._fileSystemsList.addEventListener(WebInspector.SettingsList.Events.Selected, this._fileSystemSelected.bind(this));
    438         this._fileSystemsList.addEventListener(WebInspector.SettingsList.Events.Removed, this._fileSystemRemovedfromList.bind(this));
    439         this._fileSystemsList.addEventListener(WebInspector.SettingsList.Events.DoubleClicked, this._fileSystemDoubleClicked.bind(this));
    440         this._fileSystemsListContainer.appendChild(this._fileSystemsList.element);
    441         for (var i = 0; i < fileSystemPaths.length; ++i)
    442             this._fileSystemsList.addItem(fileSystemPaths[i]);
    443         this._updateEditFileSystemButtonState();
    444     },
    445 
    446     _updateEditFileSystemButtonState: function()
    447     {
    448         this._editFileSystemButton.disabled = !this._selectedFileSystemPath();
    449     },
    450 
    451     /**
    452      * @param {!WebInspector.Event} event
    453      */
    454     _fileSystemSelected: function(event)
    455     {
    456         this._updateEditFileSystemButtonState();
    457     },
    458 
    459     /**
    460      * @param {!WebInspector.Event} event
    461      */
    462     _fileSystemDoubleClicked: function(event)
    463     {
    464         var id = /** @type{?string} */ (event.data);
    465         this._editFileSystem(id);
    466     },
    467 
    468     _editFileSystemClicked: function()
    469     {
    470         this._editFileSystem(this._selectedFileSystemPath());
    471     },
    472 
    473     /**
    474      * @param {?string} id
    475      */
    476     _editFileSystem: function(id)
    477     {
    478         WebInspector.EditFileSystemDialog.show(WebInspector.inspectorView.element, id);
    479     },
    480 
    481     /**
    482      * @param {!Element} columnElement
    483      * @param {{id: string, placeholder: (string|undefined), options: (!Array.<string>|undefined)}} column
    484      * @param {?string} id
    485      */
    486     _renderFileSystem: function(columnElement, column, id)
    487     {
    488         if (!id)
    489             return "";
    490         var fileSystemPath = id;
    491         var textElement = columnElement.createChild("span", "list-column-text");
    492         var pathElement = textElement.createChild("span", "file-system-path");
    493         pathElement.title = fileSystemPath;
    494 
    495         const maxTotalPathLength = 55;
    496         const maxFolderNameLength = 30;
    497 
    498         var lastIndexOfSlash = fileSystemPath.lastIndexOf(WebInspector.isWin() ? "\\" : "/");
    499         var folderName = fileSystemPath.substr(lastIndexOfSlash + 1);
    500         var folderPath = fileSystemPath.substr(0, lastIndexOfSlash + 1);
    501         folderPath = folderPath.trimMiddle(maxTotalPathLength - Math.min(maxFolderNameLength, folderName.length));
    502         folderName = folderName.trimMiddle(maxFolderNameLength);
    503 
    504         var folderPathElement = pathElement.createChild("span");
    505         folderPathElement.textContent = folderPath;
    506 
    507         var nameElement = pathElement.createChild("span", "file-system-path-name");
    508         nameElement.textContent = folderName;
    509     },
    510 
    511     /**
    512      * @param {!WebInspector.Event} event
    513      */
    514     _fileSystemRemovedfromList: function(event)
    515     {
    516         var id = /** @type{?string} */ (event.data);
    517         if (!id)
    518             return;
    519         WebInspector.isolatedFileSystemManager.removeFileSystem(id);
    520     },
    521 
    522     _addFileSystemClicked: function()
    523     {
    524         WebInspector.isolatedFileSystemManager.addFileSystem();
    525     },
    526 
    527     _fileSystemAdded: function(event)
    528     {
    529         var fileSystem = /** @type {!WebInspector.IsolatedFileSystem} */ (event.data);
    530         if (!this._fileSystemsList)
    531             this._reset();
    532         else
    533             this._fileSystemsList.addItem(fileSystem.path());
    534     },
    535 
    536     _fileSystemRemoved: function(event)
    537     {
    538         var fileSystem = /** @type {!WebInspector.IsolatedFileSystem} */ (event.data);
    539         var selectedFileSystemPath = this._selectedFileSystemPath();
    540         if (this._fileSystemsList.itemForId(fileSystem.path()))
    541             this._fileSystemsList.removeItem(fileSystem.path());
    542         if (!this._fileSystemsList.itemIds().length)
    543             this._reset();
    544         this._updateEditFileSystemButtonState();
    545     },
    546 
    547     _selectedFileSystemPath: function()
    548     {
    549         return this._fileSystemsList ? this._fileSystemsList.selectedId() : null;
    550     },
    551 
    552     __proto__: WebInspector.SettingsTab.prototype
    553 }
    554 
    555 
    556 /**
    557  * @constructor
    558  * @extends {WebInspector.SettingsTab}
    559  */
    560 WebInspector.ExperimentsSettingsTab = function()
    561 {
    562     WebInspector.SettingsTab.call(this, WebInspector.UIString("Experiments"), "experiments-tab-content");
    563 
    564     var experiments = Runtime.experiments.allExperiments();
    565     if (experiments.length) {
    566         var experimentsSection = this._appendSection();
    567         experimentsSection.appendChild(this._createExperimentsWarningSubsection());
    568         for (var i = 0; i < experiments.length; ++i)
    569             experimentsSection.appendChild(this._createExperimentCheckbox(experiments[i]));
    570     }
    571 }
    572 
    573 WebInspector.ExperimentsSettingsTab.prototype = {
    574     /**
    575      * @return {!Element} element
    576      */
    577     _createExperimentsWarningSubsection: function()
    578     {
    579         var subsection = document.createElement("div");
    580         var warning = subsection.createChild("span", "settings-experiments-warning-subsection-warning");
    581         warning.textContent = WebInspector.UIString("WARNING:");
    582         subsection.createTextChild(" ");
    583         var message = subsection.createChild("span", "settings-experiments-warning-subsection-message");
    584         message.textContent = WebInspector.UIString("These experiments could be dangerous and may require restart.");
    585         return subsection;
    586     },
    587 
    588     _createExperimentCheckbox: function(experiment)
    589     {
    590         var input = document.createElement("input");
    591         input.type = "checkbox";
    592         input.name = experiment.name;
    593         input.checked = experiment.isEnabled();
    594         function listener()
    595         {
    596             experiment.setEnabled(input.checked);
    597         }
    598         input.addEventListener("click", listener, false);
    599 
    600         var p = document.createElement("p");
    601         p.className = experiment.hidden && !experiment.isEnabled() ? "settings-experiment-hidden" : "";
    602         var label = p.createChild("label");
    603         label.appendChild(input);
    604         label.createTextChild(WebInspector.UIString(experiment.title));
    605         p.appendChild(label);
    606         return p;
    607     },
    608 
    609     __proto__: WebInspector.SettingsTab.prototype
    610 }
    611 
    612 /**
    613  * @constructor
    614  */
    615 WebInspector.SettingsController = function()
    616 {
    617     /** @type {?WebInspector.SettingsScreen} */
    618     this._settingsScreen;
    619 
    620     window.addEventListener("resize", this._resize.bind(this), false);
    621 }
    622 
    623 WebInspector.SettingsController.prototype = {
    624     _onHideSettingsScreen: function()
    625     {
    626         delete this._settingsScreenVisible;
    627     },
    628 
    629     /**
    630      * @param {string=} tabId
    631      */
    632     showSettingsScreen: function(tabId)
    633     {
    634         if (!this._settingsScreen)
    635             this._settingsScreen = new WebInspector.SettingsScreen(this._onHideSettingsScreen.bind(this));
    636 
    637         if (tabId)
    638             this._settingsScreen.selectTab(tabId);
    639 
    640         this._settingsScreen.showModal();
    641         this._settingsScreenVisible = true;
    642     },
    643 
    644     _resize: function()
    645     {
    646         if (this._settingsScreen && this._settingsScreen.isShowing())
    647             this._settingsScreen.doResize();
    648     }
    649 }
    650 
    651 /**
    652  * @constructor
    653  * @implements {WebInspector.ActionDelegate}
    654  */
    655 WebInspector.SettingsController.SettingsScreenActionDelegate = function() { }
    656 
    657 WebInspector.SettingsController.SettingsScreenActionDelegate.prototype = {
    658     /**
    659      * @return {boolean}
    660      */
    661     handleAction: function()
    662     {
    663         WebInspector._settingsController.showSettingsScreen(WebInspector.SettingsScreen.Tabs.General);
    664         return true;
    665     }
    666 }
    667 
    668 /**
    669  * @constructor
    670  * @extends {WebInspector.Object}
    671  * @param {!Array.<{id: string, placeholder: (string|undefined), options: (!Array.<string>|undefined)}>} columns
    672  * @param {function(!Element, {id: string, placeholder: (string|undefined), options: (!Array.<string>|undefined)}, ?string)} itemRenderer
    673  */
    674 WebInspector.SettingsList = function(columns, itemRenderer)
    675 {
    676     this.element = document.createElementWithClass("div", "settings-list");
    677     this.element.tabIndex = -1;
    678     this._itemRenderer = itemRenderer;
    679     /** @type {!StringMap.<!Element>} */
    680     this._listItems = new StringMap();
    681     /** @type {!Array.<?string>} */
    682     this._ids = [];
    683     this._columns = columns;
    684 }
    685 
    686 WebInspector.SettingsList.Events = {
    687     Selected:  "Selected",
    688     Removed:  "Removed",
    689     DoubleClicked:  "DoubleClicked",
    690 }
    691 
    692 WebInspector.SettingsList.prototype = {
    693     /**
    694      * @param {?string} itemId
    695      * @param {?string=} beforeId
    696      * @return {!Element}
    697      */
    698     addItem: function(itemId, beforeId)
    699     {
    700         var listItem = document.createElementWithClass("div", "settings-list-item");
    701         listItem._id = itemId;
    702         if (typeof beforeId !== "undefined")
    703             this.element.insertBefore(listItem, this.itemForId(beforeId));
    704         else
    705             this.element.appendChild(listItem);
    706 
    707         var listItemContents = listItem.createChild("div", "settings-list-item-contents");
    708         var listItemColumnsElement = listItemContents.createChild("div", "settings-list-item-columns");
    709 
    710         listItem.columnElements = {};
    711         for (var i = 0; i < this._columns.length; ++i) {
    712             var column = this._columns[i];
    713             var columnElement = listItemColumnsElement.createChild("div", "list-column settings-list-column-" + column.id);
    714             listItem.columnElements[column.id] = columnElement;
    715             this._itemRenderer(columnElement, column, itemId);
    716         }
    717         var removeItemButton = this._createRemoveButton(removeItemClicked.bind(this));
    718         listItemContents.addEventListener("click", this.selectItem.bind(this, itemId), false);
    719         listItemContents.addEventListener("dblclick", this._onDoubleClick.bind(this, itemId), false);
    720         listItemContents.appendChild(removeItemButton);
    721 
    722         this._listItems.set(itemId || "", listItem);
    723         if (typeof beforeId !== "undefined")
    724             this._ids.splice(this._ids.indexOf(beforeId), 0, itemId);
    725         else
    726             this._ids.push(itemId);
    727 
    728         /**
    729          * @param {!Event} event
    730          * @this {WebInspector.SettingsList}
    731          */
    732         function removeItemClicked(event)
    733         {
    734             removeItemButton.disabled = true;
    735             this.removeItem(itemId);
    736             this.dispatchEventToListeners(WebInspector.SettingsList.Events.Removed, itemId);
    737             event.consume();
    738         }
    739 
    740         return listItem;
    741     },
    742 
    743     /**
    744      * @param {?string} id
    745      */
    746     removeItem: function(id)
    747     {
    748         var listItem = this._listItems.remove(id || "");
    749         if (listItem)
    750             listItem.remove();
    751         this._ids.remove(id);
    752         if (id === this._selectedId) {
    753             delete this._selectedId;
    754             if (this._ids.length)
    755                 this.selectItem(this._ids[0]);
    756         }
    757     },
    758 
    759     /**
    760      * @return {!Array.<?string>}
    761      */
    762     itemIds: function()
    763     {
    764         return this._ids.slice();
    765     },
    766 
    767     /**
    768      * @return {!Array.<string>}
    769      */
    770     columns: function()
    771     {
    772         return this._columns.select("id");
    773     },
    774 
    775     /**
    776      * @return {?string}
    777      */
    778     selectedId: function()
    779     {
    780         return this._selectedId;
    781     },
    782 
    783     /**
    784      * @return {?Element}
    785      */
    786     selectedItem: function()
    787     {
    788         return this._selectedId ? this.itemForId(this._selectedId) : null;
    789     },
    790 
    791     /**
    792      * @param {?string} itemId
    793      * @return {?Element}
    794      */
    795     itemForId: function(itemId)
    796     {
    797         return this._listItems.get(itemId || "") || null;
    798     },
    799 
    800     /**
    801      * @param {?string} id
    802      * @param {!Event=} event
    803      */
    804     _onDoubleClick: function(id, event)
    805     {
    806         this.dispatchEventToListeners(WebInspector.SettingsList.Events.DoubleClicked, id);
    807     },
    808 
    809     /**
    810      * @param {?string} id
    811      * @param {!Event=} event
    812      */
    813     selectItem: function(id, event)
    814     {
    815         if (typeof this._selectedId !== "undefined")
    816             this.itemForId(this._selectedId).classList.remove("selected");
    817 
    818         this._selectedId = id;
    819         if (typeof this._selectedId !== "undefined")
    820             this.itemForId(this._selectedId).classList.add("selected");
    821 
    822         this.dispatchEventToListeners(WebInspector.SettingsList.Events.Selected, id);
    823         if (event)
    824             event.consume();
    825     },
    826 
    827     /**
    828      * @param {function(!Event)} handler
    829      * @return {!Element}
    830      */
    831     _createRemoveButton: function(handler)
    832     {
    833         var removeButton = document.createElementWithClass("div", "remove-item-button");
    834         removeButton.addEventListener("click", handler, false);
    835         return removeButton;
    836     },
    837 
    838     __proto__: WebInspector.Object.prototype
    839 }
    840 
    841 /**
    842  * @constructor
    843  * @extends {WebInspector.SettingsList}
    844  * @param {!Array.<{id: string, placeholder: (string|undefined), options: (!Array.<string>|undefined)}>} columns
    845  * @param {function(string, string):string} valuesProvider
    846  * @param {function(?string, !Object):!Array.<string>} validateHandler
    847  * @param {function(?string, !Object)} editHandler
    848  */
    849 WebInspector.EditableSettingsList = function(columns, valuesProvider, validateHandler, editHandler)
    850 {
    851     WebInspector.SettingsList.call(this, columns, this._renderColumn.bind(this));
    852     this._valuesProvider = valuesProvider;
    853     this._validateHandler = validateHandler;
    854     this._editHandler = editHandler;
    855     /** @type {!StringMap.<(!HTMLInputElement|!HTMLSelectElement)>} */
    856     this._addInputElements = new StringMap();
    857     /** @type {!StringMap.<!StringMap.<(!HTMLInputElement|!HTMLSelectElement)>>} */
    858     this._editInputElements = new StringMap();
    859     /** @type {!StringMap.<!StringMap.<!HTMLSpanElement>>} */
    860     this._textElements = new StringMap();
    861 
    862     this._addMappingItem = this.addItem(null);
    863     this._addMappingItem.classList.add("item-editing", "add-list-item");
    864 }
    865 
    866 WebInspector.EditableSettingsList.prototype = {
    867     /**
    868      * @param {?string} itemId
    869      * @param {?string=} beforeId
    870      * @return {!Element}
    871      */
    872     addItem: function(itemId, beforeId)
    873     {
    874         var listItem = WebInspector.SettingsList.prototype.addItem.call(this, itemId, beforeId);
    875         listItem.classList.add("editable");
    876         return listItem;
    877     },
    878 
    879     /**
    880      * @param {?string} itemId
    881      */
    882     refreshItem: function(itemId)
    883     {
    884         if (!itemId)
    885             return;
    886         var listItem = this.itemForId(itemId);
    887         if (!listItem)
    888             return;
    889         for (var i = 0; i < this._columns.length; ++i) {
    890             var column = this._columns[i];
    891             var columnId = column.id;
    892 
    893             var value = this._valuesProvider(itemId, columnId);
    894             this._setTextElementContent(itemId, columnId, value);
    895 
    896             var editElement = this._editInputElements.get(itemId).get(columnId);
    897             this._setEditElementValue(editElement, value || "");
    898         }
    899     },
    900 
    901     /**
    902      * @param {?string} itemId
    903      * @param {string} columnId
    904      */
    905     _textElementContent: function(itemId, columnId)
    906     {
    907         if (!itemId)
    908             return "";
    909         return this._textElements.get(itemId).get(columnId).textContent.replace(/\u200B/g, "");
    910     },
    911 
    912     /**
    913      * @param {string} itemId
    914      * @param {string} columnId
    915      * @param {string} text
    916      */
    917     _setTextElementContent: function(itemId, columnId, text)
    918     {
    919         var textElement = this._textElements.get(itemId).get(columnId);
    920         textElement.textContent = text.replace(/.{4}/g, "$&\u200B");
    921         textElement.title = text;
    922     },
    923 
    924     /**
    925      * @param {!Element} columnElement
    926      * @param {{id: string, placeholder: (string|undefined), options: (!Array.<string>|undefined)}} column
    927      * @param {?string} itemId
    928      */
    929     _renderColumn: function(columnElement, column, itemId)
    930     {
    931         var columnId = column.id;
    932         if (itemId === null) {
    933             this._createEditElement(columnElement, column, itemId);
    934             return;
    935         }
    936         var validItemId = itemId;
    937 
    938         if (!this._editInputElements.has(itemId))
    939             this._editInputElements.set(itemId, new StringMap());
    940         if (!this._textElements.has(itemId))
    941             this._textElements.set(itemId, new StringMap());
    942 
    943         var value = this._valuesProvider(itemId, columnId);
    944 
    945         var textElement = /** @type {!HTMLSpanElement} */ (columnElement.createChild("span", "list-column-text"));
    946         columnElement.addEventListener("click", rowClicked.bind(this), false);
    947         this._textElements.get(itemId).set(columnId, textElement);
    948         this._setTextElementContent(itemId, columnId, value);
    949 
    950         this._createEditElement(columnElement, column, itemId, value);
    951 
    952         /**
    953          * @param {!Event} event
    954          * @this {WebInspector.EditableSettingsList}
    955          */
    956         function rowClicked(event)
    957         {
    958             if (itemId === this._editingId)
    959                 return;
    960             console.assert(!this._editingId);
    961             this._editingId = validItemId;
    962             var listItem = this.itemForId(validItemId);
    963             listItem.classList.add("item-editing");
    964             var editElement = event.target.editElement || this._editInputElements.get(validItemId).get(this.columns()[0]);
    965             editElement.focus();
    966             if (editElement.select)
    967                 editElement.select();
    968         }
    969     },
    970 
    971     /**
    972      * @param {!Element} columnElement
    973      * @param {{id: string, placeholder: (string|undefined), options: (!Array.<string>|undefined)}} column
    974      * @param {?string} itemId
    975      * @param {string=} value
    976      * @return {!Element}
    977      */
    978     _createEditElement: function(columnElement, column, itemId, value)
    979     {
    980         var options = column.options;
    981         if (options) {
    982             var editElement = /** @type {!HTMLSelectElement} */ (columnElement.createChild("select", "chrome-select list-column-editor"));
    983             for (var i = 0; i < options.length; ++i) {
    984                 var option = editElement.createChild("option");
    985                 option.value = options[i];
    986                 option.textContent = options[i];
    987             }
    988             editElement.addEventListener("blur", this._editMappingBlur.bind(this, itemId), false);
    989             editElement.addEventListener("change", this._editMappingBlur.bind(this, itemId), false);
    990         } else {
    991             var editElement = /** @type {!HTMLInputElement} */ (columnElement.createChild("input", "list-column-editor"));
    992             editElement.addEventListener("blur", this._editMappingBlur.bind(this, itemId), false);
    993             editElement.addEventListener("input", this._validateEdit.bind(this, itemId), false);
    994             if (itemId === null)
    995                 editElement.placeholder = column.placeholder || "";
    996         }
    997 
    998         if (itemId === null)
    999             this._addInputElements.set(column.id, editElement);
   1000         else
   1001             this._editInputElements.get(itemId).set(column.id, editElement);
   1002 
   1003         this._setEditElementValue(editElement, value || "");
   1004         columnElement.editElement = editElement;
   1005         return editElement;
   1006     },
   1007 
   1008     /**
   1009      * @param {!HTMLInputElement|!HTMLSelectElement|undefined} editElement
   1010      * @param {string} value
   1011      */
   1012     _setEditElementValue: function(editElement, value)
   1013     {
   1014         if (!editElement)
   1015             return;
   1016         if (editElement instanceof HTMLSelectElement) {
   1017             var options = editElement.options;
   1018             for (var i = 0; i < options.length; ++i)
   1019                 options[i].selected = (options[i].value === value);
   1020         } else {
   1021             editElement.value = value;
   1022         }
   1023     },
   1024 
   1025     /**
   1026      * @param {?string} itemId
   1027      * @return {!Object}
   1028      */
   1029     _data: function(itemId)
   1030     {
   1031         var inputElements = this._inputElements(itemId);
   1032         var data = { __proto__: null };
   1033         var columns = this.columns();
   1034         for (var i = 0; i < columns.length; ++i)
   1035             data[columns[i]] = inputElements.get(columns[i]).value;
   1036         return data;
   1037     },
   1038 
   1039     /**
   1040      * @param {?string} itemId
   1041      * @return {?StringMap.<(!HTMLInputElement|!HTMLSelectElement)>}
   1042      */
   1043     _inputElements: function(itemId)
   1044     {
   1045         if (!itemId)
   1046             return this._addInputElements;
   1047         return this._editInputElements.get(itemId) || null;
   1048     },
   1049 
   1050     /**
   1051      * @param {?string} itemId
   1052      * @return {boolean}
   1053      */
   1054     _validateEdit: function(itemId)
   1055     {
   1056         var errorColumns = this._validateHandler(itemId, this._data(itemId));
   1057         var hasChanges = this._hasChanges(itemId);
   1058         var columns = this.columns();
   1059         for (var i = 0; i < columns.length; ++i) {
   1060             var columnId = columns[i];
   1061             var inputElement = this._inputElements(itemId).get(columnId);
   1062             if (hasChanges && errorColumns.indexOf(columnId) !== -1)
   1063                 inputElement.classList.add("editable-item-error");
   1064             else
   1065                 inputElement.classList.remove("editable-item-error");
   1066         }
   1067         return !errorColumns.length;
   1068     },
   1069 
   1070     /**
   1071      * @param {?string} itemId
   1072      * @return {boolean}
   1073      */
   1074     _hasChanges: function(itemId)
   1075     {
   1076         var columns = this.columns();
   1077         for (var i = 0; i < columns.length; ++i) {
   1078             var columnId = columns[i];
   1079             var oldValue = this._textElementContent(itemId, columnId);
   1080             var newValue = this._inputElements(itemId).get(columnId).value;
   1081             if (oldValue !== newValue)
   1082                 return true;
   1083         }
   1084         return false;
   1085     },
   1086 
   1087     /**
   1088      * @param {?string} itemId
   1089      * @param {!Event} event
   1090      */
   1091     _editMappingBlur: function(itemId, event)
   1092     {
   1093         if (itemId === null) {
   1094             this._onAddMappingInputBlur(event);
   1095             return;
   1096         }
   1097 
   1098         var inputElements = this._editInputElements.get(itemId).values();
   1099         if (inputElements.indexOf(event.relatedTarget) !== -1)
   1100             return;
   1101 
   1102         var listItem = this.itemForId(itemId);
   1103         listItem.classList.remove("item-editing");
   1104         delete this._editingId;
   1105 
   1106         if (!this._hasChanges(itemId))
   1107             return;
   1108 
   1109         if (!this._validateEdit(itemId)) {
   1110             var columns = this.columns();
   1111             for (var i = 0; i < columns.length; ++i) {
   1112                 var columnId = columns[i];
   1113                 var editElement = this._editInputElements.get(itemId).get(columnId);
   1114                 this._setEditElementValue(editElement, this._textElementContent(itemId, columnId));
   1115                 editElement.classList.remove("editable-item-error");
   1116             }
   1117             return;
   1118         }
   1119         this._editHandler(itemId, this._data(itemId));
   1120     },
   1121 
   1122     /**
   1123      * @param {!Event} event
   1124      */
   1125     _onAddMappingInputBlur: function(event)
   1126     {
   1127         var inputElements = this._addInputElements.values();
   1128         if (inputElements.indexOf(event.relatedTarget) !== -1)
   1129             return;
   1130 
   1131         if (!this._hasChanges(null))
   1132             return;
   1133 
   1134         if (!this._validateEdit(null))
   1135             return;
   1136 
   1137         this._editHandler(null, this._data(null));
   1138         var columns = this.columns();
   1139         for (var i = 0; i < columns.length; ++i) {
   1140             var columnId = columns[i];
   1141             var editElement = this._addInputElements.get(columnId);
   1142             this._setEditElementValue(editElement, "");
   1143         }
   1144     },
   1145 
   1146     __proto__: WebInspector.SettingsList.prototype
   1147 }
   1148 
   1149 WebInspector._settingsController = new WebInspector.SettingsController();
   1150