2c76df2ebba885a3f62b4dea3cb22a9293b4aa23
[platform/framework/web/crosswalk.git] / src / third_party / WebKit / Source / devtools / front_end / settings / SettingsScreen.js
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 (WebInspector.experimentsSettings.experimentsEnabled)
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.put(descriptor["parentSettingName"], extension);
248                 return;
249             }
250             extensionsBySectionId.put(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 && (!WebInspector.experimentsSettings[experimentName] || !WebInspector.experimentsSettings[experimentName].isEnabled()))
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 = WebInspector.experimentsSettings.experiments;
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.put(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             var textElement = this._textElements.get(itemId).get(columnId);
895             textElement.textContent = value;
896             textElement.title = value;
897
898             var editElement = this._editInputElements.get(itemId).get(columnId);
899             this._setEditElementValue(editElement, value || "");
900         }
901     },
902
903     /**
904      * @param {!Element} columnElement
905      * @param {{id: string, placeholder: (string|undefined), options: (!Array.<string>|undefined)}} column
906      * @param {?string} itemId
907      */
908     _renderColumn: function(columnElement, column, itemId)
909     {
910         var columnId = column.id;
911         if (itemId === null) {
912             this._createEditElement(columnElement, column, itemId);
913             return;
914         }
915         var validItemId = itemId;
916
917         if (!this._editInputElements.contains(itemId))
918             this._editInputElements.put(itemId, new StringMap());
919         if (!this._textElements.contains(itemId))
920             this._textElements.put(itemId, new StringMap());
921
922         var value = this._valuesProvider(itemId, columnId);
923
924         var textElement = /** @type {!HTMLSpanElement} */ (columnElement.createChild("span", "list-column-text"));
925         textElement.textContent = value;
926         textElement.title = value;
927         columnElement.addEventListener("click", rowClicked.bind(this), false);
928         this._textElements.get(itemId).put(columnId, textElement);
929
930         this._createEditElement(columnElement, column, itemId, value);
931
932         /**
933          * @param {!Event} event
934          * @this {WebInspector.EditableSettingsList}
935          */
936         function rowClicked(event)
937         {
938             if (itemId === this._editingId)
939                 return;
940             event.consume();
941             console.assert(!this._editingId);
942             this._editingId = validItemId;
943             var listItem = this.itemForId(validItemId);
944             listItem.classList.add("item-editing");
945             var editElement = event.target.editElement || this._editInputElements.get(validItemId).get(this.columns()[0]);
946             editElement.focus();
947             if (editElement.select)
948                 editElement.select();
949         }
950     },
951
952     /**
953      * @param {!Element} columnElement
954      * @param {{id: string, placeholder: (string|undefined), options: (!Array.<string>|undefined)}} column
955      * @param {?string} itemId
956      * @param {string=} value
957      * @return {!Element}
958      */
959     _createEditElement: function(columnElement, column, itemId, value)
960     {
961         var options = column.options;
962         if (options) {
963             var editElement = /** @type {!HTMLSelectElement} */ (columnElement.createChild("select", "chrome-select list-column-editor"));
964             for (var i = 0; i < options.length; ++i) {
965                 var option = editElement.createChild("option");
966                 option.value = options[i];
967                 option.textContent = options[i];
968             }
969             editElement.addEventListener("blur", this._editMappingBlur.bind(this, itemId), false);
970             editElement.addEventListener("change", this._editMappingBlur.bind(this, itemId), false);
971         } else {
972             var editElement = /** @type {!HTMLInputElement} */ (columnElement.createChild("input", "list-column-editor"));
973             editElement.addEventListener("blur", this._editMappingBlur.bind(this, itemId), false);
974             editElement.addEventListener("input", this._validateEdit.bind(this, itemId), false);
975             if (itemId === null)
976                 editElement.placeholder = column.placeholder || "";
977         }
978
979         if (itemId === null)
980             this._addInputElements.put(column.id, editElement);
981         else
982             this._editInputElements.get(itemId).put(column.id, editElement);
983
984         this._setEditElementValue(editElement, value || "");
985         columnElement.editElement = editElement;
986         return editElement;
987     },
988
989     /**
990      * @param {!HTMLInputElement|!HTMLSelectElement|undefined} editElement
991      * @param {string} value
992      */
993     _setEditElementValue: function(editElement, value)
994     {
995         if (!editElement)
996             return;
997         if (editElement instanceof HTMLSelectElement) {
998             var options = editElement.options;
999             for (var i = 0; i < options.length; ++i)
1000                 options[i].selected = (options[i].value === value);
1001         } else {
1002             editElement.value = value;
1003         }
1004     },
1005
1006     /**
1007      * @param {?string} itemId
1008      * @return {!Object}
1009      */
1010     _data: function(itemId)
1011     {
1012         var inputElements = this._inputElements(itemId);
1013         var data = { __proto__: null };
1014         var columns = this.columns();
1015         for (var i = 0; i < columns.length; ++i)
1016             data[columns[i]] = inputElements.get(columns[i]).value;
1017         return data;
1018     },
1019
1020     /**
1021      * @param {?string} itemId
1022      * @return {?StringMap.<(!HTMLInputElement|!HTMLSelectElement)>}
1023      */
1024     _inputElements: function(itemId)
1025     {
1026         if (!itemId)
1027             return this._addInputElements;
1028         return this._editInputElements.get(itemId) || null;
1029     },
1030
1031     /**
1032      * @param {?string} itemId
1033      * @return {boolean}
1034      */
1035     _validateEdit: function(itemId)
1036     {
1037         var errorColumns = this._validateHandler(itemId, this._data(itemId));
1038         var hasChanges = this._hasChanges(itemId);
1039         var columns = this.columns();
1040         for (var i = 0; i < columns.length; ++i) {
1041             var columnId = columns[i];
1042             var inputElement = this._inputElements(itemId).get(columnId);
1043             if (hasChanges && errorColumns.indexOf(columnId) !== -1)
1044                 inputElement.classList.add("editable-item-error");
1045             else
1046                 inputElement.classList.remove("editable-item-error");
1047         }
1048         return !errorColumns.length;
1049     },
1050
1051     /**
1052      * @param {?string} itemId
1053      * @return {boolean}
1054      */
1055     _hasChanges: function(itemId)
1056     {
1057         var columns = this.columns();
1058         for (var i = 0; i < columns.length; ++i) {
1059             var columnId = columns[i];
1060             var oldValue = itemId ? this._textElements.get(itemId).get(columnId).textContent : "";
1061             var newValue = this._inputElements(itemId).get(columnId).value;
1062             if (oldValue !== newValue)
1063                 return true;
1064         }
1065         return false;
1066     },
1067
1068     /**
1069      * @param {?string} itemId
1070      * @param {!Event} event
1071      */
1072     _editMappingBlur: function(itemId, event)
1073     {
1074         if (itemId === null) {
1075             this._onAddMappingInputBlur(event);
1076             return;
1077         }
1078
1079         var inputElements = this._editInputElements.get(itemId).values();
1080         if (inputElements.indexOf(event.relatedTarget) !== -1)
1081             return;
1082
1083         var listItem = this.itemForId(itemId);
1084         listItem.classList.remove("item-editing");
1085         delete this._editingId;
1086
1087         if (!this._hasChanges(itemId))
1088             return;
1089
1090         if (!this._validateEdit(itemId)) {
1091             var columns = this.columns();
1092             for (var i = 0; i < columns.length; ++i) {
1093                 var columnId = columns[i];
1094                 var editElement = this._editInputElements.get(itemId).get(columnId);
1095                 this._setEditElementValue(editElement, this._textElements.get(itemId).get(columnId).textContent);
1096                 editElement.classList.remove("editable-item-error");
1097             }
1098             return;
1099         }
1100         this._editHandler(itemId, this._data(itemId));
1101     },
1102
1103     /**
1104      * @param {!Event} event
1105      */
1106     _onAddMappingInputBlur: function(event)
1107     {
1108         var inputElements = this._addInputElements.values();
1109         if (inputElements.indexOf(event.relatedTarget) !== -1)
1110             return;
1111
1112         if (!this._hasChanges(null))
1113             return;
1114
1115         if (!this._validateEdit(null))
1116             return;
1117
1118         this._editHandler(null, this._data(null));
1119         var columns = this.columns();
1120         for (var i = 0; i < columns.length; ++i) {
1121             var columnId = columns[i];
1122             var editElement = this._addInputElements.get(columnId);
1123             this._setEditElementValue(editElement, "");
1124         }
1125     },
1126
1127     __proto__: WebInspector.SettingsList.prototype
1128 }
1129
1130 WebInspector._settingsController = new WebInspector.SettingsController();