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 */
36WebInspector.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 */
71WebInspector.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
81WebInspector.SettingsScreen.Tabs = {
82    General: "general",
83    Overrides: "overrides",
84    Workspace: "workspace",
85    Experiments: "experiments",
86    Shortcuts: "shortcuts"
87}
88
89WebInspector.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 */
155WebInspector.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
166WebInspector.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 */
211WebInspector.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
230WebInspector.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 */
351WebInspector.SettingsScreen.SkipStackFramePatternSettingDelegate = function()
352{
353    WebInspector.UISettingDelegate.call(this);
354}
355
356WebInspector.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 */
383WebInspector.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
411WebInspector.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 */
560WebInspector.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
573WebInspector.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 */
615WebInspector.SettingsController = function()
616{
617    /** @type {?WebInspector.SettingsScreen} */
618    this._settingsScreen;
619
620    window.addEventListener("resize", this._resize.bind(this), false);
621}
622
623WebInspector.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 */
655WebInspector.SettingsController.SettingsScreenActionDelegate = function() { }
656
657WebInspector.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 */
674WebInspector.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
686WebInspector.SettingsList.Events = {
687    Selected:  "Selected",
688    Removed:  "Removed",
689    DoubleClicked:  "DoubleClicked",
690}
691
692WebInspector.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 */
849WebInspector.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
866WebInspector.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
1149WebInspector._settingsController = new WebInspector.SettingsController();
1150