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