blob: c4a972afacf53562173abd0a49c798228a79d827 [file] [log] [blame]
/*
* Copyright (C) 2009 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import * as Platform from '../platform/platform.js'; // eslint-disable-line no-unused-vars
import {ls} from '../platform/platform.js';
import * as Root from '../root/root.js';
import {Color, Format} from './Color.js'; // eslint-disable-line no-unused-vars
import {Console} from './Console.js';
import {EventDescriptor, EventTargetEvent} from './EventTarget.js'; // eslint-disable-line no-unused-vars
import {ObjectWrapper} from './Object.js';
import {getRegisteredSettings, RegExpSettingItem, registerSettingExtension, SettingCategory, SettingCategoryObject, SettingExtensionOption, SettingRegistration, SettingType, SettingTypeObject} from './SettingRegistration.js'; // eslint-disable-line no-unused-vars
/**
* @type {!Settings|undefined}
*/
let settingsInstance;
export class Settings {
/**
* @private
* @param {!SettingsStorage} globalStorage
* @param {!SettingsStorage} localStorage
*/
constructor(globalStorage, localStorage) {
this._globalStorage = globalStorage;
this._localStorage = localStorage;
this._sessionStorage = new SettingsStorage({});
/** @type {!Set<string>} */
this.settingNameSet = new Set();
/** @type {!Map<!SettingCategory,!Set<number>>} */
this.orderValuesBySettingCategory = new Map();
this._eventSupport = new ObjectWrapper();
/** @type {!Map<string, !Setting<*>>} */
this._registry = new Map();
/** @type {!Map<string, !Setting<*>>} */
this._moduleSettings = new Map();
const unionOfSettings = [
// This lookup is done for the legacy settings.
// TODO(crbug.com/1134103): remove this call once all settings have been migrated.
...Root.Runtime.Runtime.instance().extensions('setting').map(extension => {
const descriptor = extension.descriptor();
let storageType;
switch (descriptor.storageType) {
case 'local':
storageType = SettingStorageType.Local;
break;
case 'session':
storageType = SettingStorageType.Session;
break;
case 'global':
storageType = SettingStorageType.Global;
break;
default:
storageType = SettingStorageType.Global;
}
const {settingName, defaultValue, userActionCondition} = descriptor;
const setting = this.createSetting(settingName, defaultValue, storageType);
if (extension.title()) {
setting.setTitle(extension.title());
}
if (userActionCondition) {
setting.setRequiresUserAction(Boolean(Root.Runtime.Runtime.queryParam(userActionCondition)));
}
setting._extension = extension;
return setting;
}),
...getRegisteredSettings().map(registration => {
const {settingName, defaultValue, storageType} = registration;
const isRegex = registration.settingType === SettingTypeObject.REGEX;
const setting = isRegex && typeof defaultValue === 'string' ?
this.createRegExpSetting(settingName, defaultValue, undefined, storageType) :
this.createPreRegisteredSetting(settingName, defaultValue, storageType);
if (registration.titleMac) {
setting.setTitleFunction(registration.titleMac);
} else {
setting.setTitleFunction(registration.title);
}
if (registration.userActionCondition) {
setting.setRequiresUserAction(Boolean(Root.Runtime.Runtime.queryParam(registration.userActionCondition)));
}
setting.setRegistration(registration);
return setting;
})
];
unionOfSettings.forEach(this._registerModuleSetting.bind(this));
}
static hasInstance() {
return typeof settingsInstance !== 'undefined';
}
/**
* @param {{forceNew: ?boolean, globalStorage: ?SettingsStorage, localStorage: ?SettingsStorage}} opts
*/
static instance(opts = {forceNew: null, globalStorage: null, localStorage: null}) {
const {forceNew, globalStorage, localStorage} = opts;
if (!settingsInstance || forceNew) {
if (!globalStorage || !localStorage) {
throw new Error(`Unable to create settings: global and local storage must be provided: ${new Error().stack}`);
}
settingsInstance = new Settings(globalStorage, localStorage);
}
return settingsInstance;
}
static removeInstance() {
settingsInstance = undefined;
}
/**
* @param {!Setting<*>} setting
*/
_registerModuleSetting(setting) {
const settingName = setting.name;
const category = setting.category();
const order = setting.order();
if (this.settingNameSet.has(settingName)) {
throw new Error(`Duplicate Setting name '${settingName}'`);
}
if (category && order) {
const orderValues = this.orderValuesBySettingCategory.get(category) || new Set();
if (orderValues.has(order)) {
throw new Error(`Duplicate order value '${order}' for settings category '${category}'`);
}
orderValues.add(order);
this.orderValuesBySettingCategory.set(category, orderValues);
}
this.settingNameSet.add(settingName);
this._moduleSettings.set(setting.name, setting);
}
/**
* @param {string} settingName
* @return {!Setting<*>}
*/
moduleSetting(settingName) {
const setting = this._moduleSettings.get(settingName);
if (!setting) {
throw new Error('No setting registered: ' + settingName);
}
return setting;
}
/**
* @param {string} settingName
* @return {!Setting<*>}
*/
settingForTest(settingName) {
const setting = this._registry.get(settingName);
if (!setting) {
throw new Error('No setting registered: ' + settingName);
}
return setting;
}
/**
* @param {string} key
* @param {*} defaultValue
* @param {!SettingStorageType=} storageType
* @return {!LegacySetting<*>}
*/
createSetting(key, defaultValue, storageType) {
const storage = this._storageFromType(storageType);
let setting = /** @type {!LegacySetting<*>} */ (this._registry.get(key));
if (!setting) {
// TODO(crbug.com/1134103): This has to instatiate a PreRegisteredSetting instead once all settings are migrated
setting = new LegacySetting(this, key, defaultValue, this._eventSupport, storage);
this._registry.set(key, setting);
}
return setting;
}
/**
* @param {string} key
* @param {*} defaultValue
* @param {!SettingStorageType=} storageType
* @return {!PreRegisteredSetting<*>}
*/
createPreRegisteredSetting(key, defaultValue, storageType) {
// TODO(crbug.com/1134103): This function has to be removed when all settings are migrated createSetting() instantiates a PreRegisteredSetting
const storage = this._storageFromType(storageType);
let setting = /** @type {!PreRegisteredSetting<*>} */ (this._registry.get(key));
if (!setting) {
setting = new PreRegisteredSetting(this, key, defaultValue, this._eventSupport, storage);
this._registry.set(key, setting);
}
return setting;
}
/**
* @param {string} key
* @param {*} defaultValue
* @return {!Setting<*>}
*/
createLocalSetting(key, defaultValue) {
return this.createSetting(key, defaultValue, SettingStorageType.Local);
}
/**
* @param {string} key
* @param {string} defaultValue
* @param {string=} regexFlags
* @param {!SettingStorageType=} storageType
* @return {!RegExpSetting}
*/
createRegExpSetting(key, defaultValue, regexFlags, storageType) {
if (!this._registry.get(key)) {
this._registry.set(
key,
new RegExpSetting(
this, key, defaultValue, this._eventSupport, this._storageFromType(storageType), regexFlags));
}
return /** @type {!RegExpSetting} */ (this._registry.get(key));
}
clearAll() {
this._globalStorage.removeAll();
this._localStorage.removeAll();
const versionSetting = Settings.instance().createSetting(VersionController._currentVersionName, 0);
versionSetting.set(VersionController.currentVersion);
}
/**
* @param {!SettingStorageType=} storageType
* @return {!SettingsStorage}
*/
_storageFromType(storageType) {
switch (storageType) {
case (SettingStorageType.Local):
return this._localStorage;
case (SettingStorageType.Session):
return this._sessionStorage;
case (SettingStorageType.Global):
return this._globalStorage;
}
return this._globalStorage;
}
}
export class SettingsStorage {
/**
* @param {!Object<string,string>} object
* @param {function(string, string):void=} setCallback
* @param {function(string):void=} removeCallback
* @param {function(string=):void=} removeAllCallback
* @param {string=} storagePrefix
*/
constructor(object, setCallback, removeCallback, removeAllCallback, storagePrefix) {
this._object = object;
this._setCallback = setCallback || function() {};
this._removeCallback = removeCallback || function() {};
this._removeAllCallback = removeAllCallback || function() {};
this._storagePrefix = storagePrefix || '';
}
/**
* @param {string} name
* @param {string} value
*/
set(name, value) {
name = this._storagePrefix + name;
this._object[name] = value;
this._setCallback(name, value);
}
/**
* @param {string} name
* @return {boolean}
*/
has(name) {
name = this._storagePrefix + name;
return name in this._object;
}
/**
* @param {string} name
* @return {string}
*/
get(name) {
name = this._storagePrefix + name;
return this._object[name];
}
/**
* @param {string} name
*/
remove(name) {
name = this._storagePrefix + name;
delete this._object[name];
this._removeCallback(name);
}
removeAll() {
this._object = {};
this._removeAllCallback();
}
_dumpSizes() {
Console.instance().log('Ten largest settings: ');
/** @type {!Object<string,number>} */
// @ts-ignore __proto__ optimization
const sizes = {__proto__: null};
for (const key in this._object) {
sizes[key] = this._object[key].length;
}
const keys = Object.keys(sizes);
/**
* @param {string} key1
* @param {string} key2
*/
function comparator(key1, key2) {
return sizes[key2] - sizes[key1];
}
keys.sort(comparator);
for (let i = 0; i < 10 && i < keys.length; ++i) {
Console.instance().log('Setting: \'' + keys[i] + '\', size: ' + sizes[keys[i]]);
}
}
}
/**
* @template V
*/
export class Setting {
/**
* @param {function(!EventTargetEvent):void} listener
* @param {!Object=} thisObject
* @return {!EventDescriptor}
*/
addChangeListener(listener, thisObject) {
throw new Error('not implemented');
}
/**
* @param {function(!EventTargetEvent):void} listener
* @param {!Object=} thisObject
*/
removeChangeListener(listener, thisObject) {
throw new Error('not implemented');
}
/**
* @return {string}
*/
get name() {
throw new Error('not implemented');
}
/**
* @return {string}
*/
title() {
throw new Error('not implemented');
}
/**
* @param {string=} title
*/
setTitle(title) {
throw new Error('not implemented');
}
/**
* @param {boolean} requiresUserAction
*/
setRequiresUserAction(requiresUserAction) {
throw new Error('not implemented');
}
/**
* @return {V}
*/
get() {
throw new Error('not implemented');
}
/**
* @param {V} value
*/
set(value) {
throw new Error('not implemented');
}
remove() {
throw new Error('not implemented');
}
/**
* @return {?SettingType}
*/
type() {
throw new Error('not implemented');
}
/**
* @return {!Array<!SimpleSettingOption>}
*/
options() {
throw new Error('not implemented');
}
/**
* @return {?boolean}
*/
reloadRequired() {
throw new Error('not implemented');
}
/**
* @return {?SettingCategory}
*/
category() {
throw new Error('not implemented');
}
/**
* @return {?string}
*/
tags() {
throw new Error('not implemented');
}
/**
* @return {?number}
*/
order() {
throw new Error('not implemented');
}
/**
* @param {string} message
* @param {string} name
* @param {string} value
*/
_printSettingsSavingError(message, name, value) {
throw new Error('not implemented');
}
}
/**
* @extends {Setting<V>}
* @template V
*/
export class LegacySetting extends Setting {
/**
* @param {!Settings} settings
* @param {string} name
* @param {V} defaultValue
* @param {!ObjectWrapper} eventSupport
* @param {!SettingsStorage} storage
*/
constructor(settings, name, defaultValue, eventSupport, storage) {
super();
this._settings = settings;
this._name = name;
this._defaultValue = defaultValue;
this._eventSupport = eventSupport;
this._storage = storage;
/** @type {string} */
this._title = '';
/** @type {?Root.Runtime.Extension} */
this._extension = null;
}
/**
* @override
* @param {function(!EventTargetEvent):void} listener
* @param {!Object=} thisObject
* @return {!EventDescriptor}
*/
addChangeListener(listener, thisObject) {
return this._eventSupport.addEventListener(this._name, listener, thisObject);
}
/**
* @override
* @param {function(!EventTargetEvent):void} listener
* @param {!Object=} thisObject
*/
removeChangeListener(listener, thisObject) {
this._eventSupport.removeEventListener(this._name, listener, thisObject);
}
/**
* @override
*/
get name() {
return this._name;
}
/**
* @override
* @return {string}
*/
title() {
return ls(this._title);
}
/**
* @override
* @param {string=} title
*/
setTitle(title) {
if (title) {
this._title = title;
}
}
/**
* @override
* @param {boolean} requiresUserAction
*/
setRequiresUserAction(requiresUserAction) {
this._requiresUserAction = requiresUserAction;
}
/**
* @override
* @return {V}
*/
get() {
if (this._requiresUserAction && !this._hadUserAction) {
return this._defaultValue;
}
if (typeof this._value !== 'undefined') {
return this._value;
}
this._value = this._defaultValue;
if (this._storage.has(this._name)) {
try {
this._value = JSON.parse(this._storage.get(this._name));
} catch (e) {
this._storage.remove(this._name);
}
}
return this._value;
}
/**
* @override
* @param {V} value
*/
set(value) {
this._hadUserAction = true;
this._value = value;
try {
const settingString = JSON.stringify(value);
try {
this._storage.set(this._name, settingString);
} catch (e) {
this._printSettingsSavingError(e.message, this._name, settingString);
}
} catch (e) {
Console.instance().error('Cannot stringify setting with name: ' + this._name + ', error: ' + e.message);
}
this._eventSupport.dispatchEventToListeners(this._name, value);
}
/**
* @override
*/
remove() {
this._settings._registry.delete(this._name);
this._settings._moduleSettings.delete(this._name);
this._storage.remove(this._name);
}
/**
* @override
* @return {?SettingType}
*/
type() {
if (this._extension) {
const type = this._extension.descriptor().settingType;
switch (type) {
case 'boolean':
return SettingTypeObject.BOOLEAN;
case 'enum':
return SettingTypeObject.ENUM;
case 'regex':
return SettingTypeObject.REGEX;
case 'array':
return SettingTypeObject.ARRAY;
default:
throw new Error('Invalid setting type');
}
}
return null;
}
/**
* @override
* @return {!Array<!SimpleSettingOption>}
*/
options() {
if (this._extension) {
const options = this._extension.descriptor().options;
if (!options) {
return [];
}
return options.map(opt => {
let text;
if (opt.text) {
// The "raw" flag indicates text is non-i18n-izable.
text = opt.raw ? opt.text : ls(opt.text);
}
return {
...opt,
text,
title: ls(opt.title),
};
});
}
return [];
}
/**
* @override
* @return {?boolean}
*/
reloadRequired() {
if (this._extension) {
return this._extension.descriptor().reloadRequired || null;
}
return null;
}
/**
* @override
* @return {?SettingCategory}
*/
category() {
if (this._extension) {
const category = this._extension.descriptor().category;
if (!category) {
return null;
}
switch (category) {
case 'Elements':
return SettingCategoryObject.ELEMENTS;
case 'Appearance':
return SettingCategoryObject.APPEARANCE;
case 'Sources':
return SettingCategoryObject.SOURCES;
case 'Network':
return SettingCategoryObject.NETWORK;
case 'Performance':
return SettingCategoryObject.PERFORMANCE;
case 'Console':
return SettingCategoryObject.CONSOLE;
case 'Persistence':
return SettingCategoryObject.PERSISTENCE;
case 'Debugger':
return SettingCategoryObject.DEBUGGER;
case 'Global':
return SettingCategoryObject.GLOBAL;
case 'Rendering':
return SettingCategoryObject.RENDERING;
case 'Grid':
return SettingCategoryObject.GRID;
case 'Mobile':
return SettingCategoryObject.MOBILE;
case 'Emulation':
return SettingCategoryObject.EMULATION;
case 'Memory':
return SettingCategoryObject.MEMORY;
default:
throw new Error(`Invalid setting category ${category}`);
}
}
return null;
}
/**
* @override
* @return {?string}
*/
tags() {
if (this._extension) {
const tags = this._extension.descriptor().tags;
if (!tags) {
return null;
}
// Get localized keys and separate by null character to prevent fuzzy matching from matching across them.
const keyList = tags.split(',');
let keys = '';
keyList.forEach(k => {
keys += (ls(k.trim()) + '\0');
});
return keys;
}
return null;
}
/**
* @override
* @return {?number}
*/
order() {
if (this._extension) {
return this._extension.descriptor().order || null;
}
return null;
}
/**
* @override
* @param {string} message
* @param {string} name
* @param {string} value
*/
_printSettingsSavingError(message, name, value) {
const errorMessage =
'Error saving setting with name: ' + this._name + ', value length: ' + value.length + '. Error: ' + message;
console.error(errorMessage);
Console.instance().error(errorMessage);
this._storage._dumpSizes();
}
}
/**
* @extends {Setting<V>}
* @template V
*/
export class PreRegisteredSetting extends Setting {
/**
* @param {!Settings} settings
* @param {string} name
* @param {V} defaultValue
* @param {!ObjectWrapper} eventSupport
* @param {!SettingsStorage} storage
*/
constructor(settings, name, defaultValue, eventSupport, storage) {
super();
this._settings = settings;
this._name = name;
this._defaultValue = defaultValue;
this._eventSupport = eventSupport;
this._storage = storage;
/** @type {function():Platform.UIString.LocalizedString} */
this._titleFunction;
/** @type {?SettingRegistration} */
this._registration = null;
}
/**
* @override
* @param {function(!EventTargetEvent):void} listener
* @param {!Object=} thisObject
* @return {!EventDescriptor}
*/
addChangeListener(listener, thisObject) {
return this._eventSupport.addEventListener(this._name, listener, thisObject);
}
/**
* @override
* @param {function(!EventTargetEvent):void} listener
* @param {!Object=} thisObject
*/
removeChangeListener(listener, thisObject) {
this._eventSupport.removeEventListener(this._name, listener, thisObject);
}
/**
* @override
*/
get name() {
return this._name;
}
/**
* @override
* @return {Platform.UIString.LocalizedString}
*/
title() {
if (!this._titleFunction) {
return /** @type {Platform.UIString.LocalizedString} */ ('');
}
return this._titleFunction();
}
/**
* @override
* @param {undefined|function():Platform.UIString.LocalizedString} titleFunction
*/
setTitleFunction(titleFunction) {
if (titleFunction) {
this._titleFunction = titleFunction;
}
}
/**
* @override
* @param {boolean} requiresUserAction
*/
setRequiresUserAction(requiresUserAction) {
this._requiresUserAction = requiresUserAction;
}
/**
* @override
* @return {V}
*/
get() {
if (this._requiresUserAction && !this._hadUserAction) {
return this._defaultValue;
}
if (typeof this._value !== 'undefined') {
return this._value;
}
this._value = this._defaultValue;
if (this._storage.has(this._name)) {
try {
this._value = JSON.parse(this._storage.get(this._name));
} catch (e) {
this._storage.remove(this._name);
}
}
return this._value;
}
/**
* @override
* @param {V} value
*/
set(value) {
this._hadUserAction = true;
this._value = value;
try {
const settingString = JSON.stringify(value);
try {
this._storage.set(this._name, settingString);
} catch (e) {
this._printSettingsSavingError(e.message, this._name, settingString);
}
} catch (e) {
Console.instance().error('Cannot stringify setting with name: ' + this._name + ', error: ' + e.message);
}
this._eventSupport.dispatchEventToListeners(this._name, value);
}
/**
* @override
*/
remove() {
this._settings._registry.delete(this._name);
this._settings._moduleSettings.delete(this._name);
this._storage.remove(this._name);
}
/**
* @param {!SettingRegistration} registration
*/
setRegistration(registration) {
this._registration = registration;
}
/**
* @override
* @return {?SettingType}
*/
type() {
if (this._registration) {
return this._registration.settingType;
}
return null;
}
/**
* @override
* @return {!Array<!SimpleSettingOption>}
*/
options() {
if (this._registration && this._registration.options) {
return this._registration.options.map(opt => {
const {value, title, text, raw} = opt;
return {
value: value,
title: title(),
text: typeof text === 'function' ? text() : text,
raw: raw,
};
});
}
return [];
}
/**
* @override
* @return {?boolean}
*/
reloadRequired() {
if (this._registration) {
return this._registration.reloadRequired || null;
}
return null;
}
/**
* @override
* @return {?SettingCategory}
*/
category() {
if (this._registration) {
return this._registration.category || null;
}
return null;
}
/**
* @override
* @return {?string}
*/
tags() {
if (this._registration && this._registration.tags) {
// Get localized keys and separate by null character to prevent fuzzy matching from matching across them.
return this._registration.tags.map(tag => tag()).join('\0');
}
return null;
}
/**
* @override
* @return {?number}
*/
order() {
if (this._registration) {
return this._registration.order || null;
}
return null;
}
/**
* @override
* @param {string} message
* @param {string} name
* @param {string} value
*/
_printSettingsSavingError(message, name, value) {
const errorMessage =
'Error saving setting with name: ' + this._name + ', value length: ' + value.length + '. Error: ' + message;
console.error(errorMessage);
Console.instance().error(errorMessage);
this._storage._dumpSizes();
}
}
/**
* @extends PreRegisteredSetting<*>
*/
export class RegExpSetting extends PreRegisteredSetting {
/**
* @param {!Settings} settings
* @param {string} name
* @param {string} defaultValue
* @param {!ObjectWrapper} eventSupport
* @param {!SettingsStorage} storage
* @param {string=} regexFlags
*/
constructor(settings, name, defaultValue, eventSupport, storage, regexFlags) {
super(settings, name, defaultValue ? [{pattern: defaultValue}] : [], eventSupport, storage);
this._regexFlags = regexFlags;
}
/**
* @override
* @return {string}
*/
get() {
const result = [];
const items = this.getAsArray();
for (let i = 0; i < items.length; ++i) {
const item = items[i];
if (item.pattern && !item.disabled) {
result.push(item.pattern);
}
}
return result.join('|');
}
/**
* @return {!Array.<!RegExpSettingItem>}
*/
getAsArray() {
return super.get();
}
/**
* @override
* @param {string} value
*/
set(value) {
this.setAsArray([{pattern: value, disabled: false}]);
}
/**
* @param {!Array.<!RegExpSettingItem>} value
*/
setAsArray(value) {
delete this._regex;
super.set(value);
}
/**
* @return {?RegExp}
*/
asRegExp() {
if (typeof this._regex !== 'undefined') {
return this._regex;
}
this._regex = null;
try {
const pattern = this.get();
if (pattern) {
this._regex = new RegExp(pattern, this._regexFlags || '');
}
} catch (e) {
}
return this._regex;
}
}
export class VersionController {
static get _currentVersionName() {
return 'inspectorVersion';
}
static get currentVersion() {
return 30;
}
updateVersion() {
const localStorageVersion = window.localStorage ? window.localStorage[VersionController._currentVersionName] : 0;
const versionSetting = Settings.instance().createSetting(VersionController._currentVersionName, 0);
const currentVersion = VersionController.currentVersion;
const oldVersion = versionSetting.get() || parseInt(localStorageVersion || '0', 10);
if (oldVersion === 0) {
// First run, no need to do anything.
versionSetting.set(currentVersion);
return;
}
const methodsToRun = this._methodsToRunToUpdateVersion(oldVersion, currentVersion);
for (const method of methodsToRun) {
// @ts-ignore Special version method matching
this[method].call(this);
}
versionSetting.set(currentVersion);
}
/**
* @param {number} oldVersion
* @param {number} currentVersion
*/
_methodsToRunToUpdateVersion(oldVersion, currentVersion) {
const result = [];
for (let i = oldVersion; i < currentVersion; ++i) {
result.push('_updateVersionFrom' + i + 'To' + (i + 1));
}
return result;
}
_updateVersionFrom0To1() {
this._clearBreakpointsWhenTooMany(Settings.instance().createLocalSetting('breakpoints', []), 500000);
}
_updateVersionFrom1To2() {
Settings.instance().createSetting('previouslyViewedFiles', []).set([]);
}
_updateVersionFrom2To3() {
Settings.instance().createSetting('fileSystemMapping', {}).set({});
Settings.instance().createSetting('fileMappingEntries', []).remove();
}
_updateVersionFrom3To4() {
const advancedMode = Settings.instance().createSetting('showHeaSnapshotObjectsHiddenProperties', false);
moduleSetting('showAdvancedHeapSnapshotProperties').set(advancedMode.get());
advancedMode.remove();
}
_updateVersionFrom4To5() {
/** @type {!Object<string,string>} */
const settingNames = {
'FileSystemViewSidebarWidth': 'fileSystemViewSplitViewState',
'elementsSidebarWidth': 'elementsPanelSplitViewState',
'StylesPaneSplitRatio': 'stylesPaneSplitViewState',
'heapSnapshotRetainersViewSize': 'heapSnapshotSplitViewState',
'InspectorView.splitView': 'InspectorView.splitViewState',
'InspectorView.screencastSplitView': 'InspectorView.screencastSplitViewState',
'Inspector.drawerSplitView': 'Inspector.drawerSplitViewState',
'layerDetailsSplitView': 'layerDetailsSplitViewState',
'networkSidebarWidth': 'networkPanelSplitViewState',
'sourcesSidebarWidth': 'sourcesPanelSplitViewState',
'scriptsPanelNavigatorSidebarWidth': 'sourcesPanelNavigatorSplitViewState',
'sourcesPanelSplitSidebarRatio': 'sourcesPanelDebuggerSidebarSplitViewState',
'timeline-details': 'timelinePanelDetailsSplitViewState',
'timeline-split': 'timelinePanelRecorsSplitViewState',
'timeline-view': 'timelinePanelTimelineStackSplitViewState',
'auditsSidebarWidth': 'auditsPanelSplitViewState',
'layersSidebarWidth': 'layersPanelSplitViewState',
'profilesSidebarWidth': 'profilesPanelSplitViewState',
'resourcesSidebarWidth': 'resourcesPanelSplitViewState'
};
const empty = {};
for (const oldName in settingNames) {
const newName = settingNames[oldName];
const oldNameH = oldName + 'H';
/** @type {?Object<string,*>} */
let newValue = null;
const oldSetting = Settings.instance().createSetting(oldName, empty);
if (oldSetting.get() !== empty) {
newValue = newValue || {};
newValue.vertical = {};
newValue.vertical.size = oldSetting.get();
oldSetting.remove();
}
const oldSettingH = Settings.instance().createSetting(oldNameH, empty);
if (oldSettingH.get() !== empty) {
newValue = newValue || {};
newValue.horizontal = {};
newValue.horizontal.size = oldSettingH.get();
oldSettingH.remove();
}
if (newValue) {
Settings.instance().createSetting(newName, {}).set(newValue);
}
}
}
_updateVersionFrom5To6() {
/** @type {!Object<string,string>} */
const settingNames = {
'debuggerSidebarHidden': 'sourcesPanelSplitViewState',
'navigatorHidden': 'sourcesPanelNavigatorSplitViewState',
'WebInspector.Drawer.showOnLoad': 'Inspector.drawerSplitViewState'
};
for (const oldName in settingNames) {
const oldSetting = Settings.instance().createSetting(oldName, null);
if (oldSetting.get() === null) {
oldSetting.remove();
continue;
}
const newName = settingNames[oldName];
const invert = oldName === 'WebInspector.Drawer.showOnLoad';
const hidden = oldSetting.get() !== invert;
oldSetting.remove();
const showMode = hidden ? 'OnlyMain' : 'Both';
const newSetting = Settings.instance().createSetting(newName, {});
const newValue = newSetting.get() || {};
newValue.vertical = newValue.vertical || {};
newValue.vertical.showMode = showMode;
newValue.horizontal = newValue.horizontal || {};
newValue.horizontal.showMode = showMode;
newSetting.set(newValue);
}
}
_updateVersionFrom6To7() {
const settingNames = {
'sourcesPanelNavigatorSplitViewState': 'sourcesPanelNavigatorSplitViewState',
'elementsPanelSplitViewState': 'elementsPanelSplitViewState',
'stylesPaneSplitViewState': 'stylesPaneSplitViewState',
'sourcesPanelDebuggerSidebarSplitViewState': 'sourcesPanelDebuggerSidebarSplitViewState'
};
const empty = {};
for (const name in settingNames) {
const setting = Settings.instance().createSetting(name, empty);
const value = setting.get();
if (value === empty) {
continue;
}
// Zero out saved percentage sizes, and they will be restored to defaults.
if (value.vertical && value.vertical.size && value.vertical.size < 1) {
value.vertical.size = 0;
}
if (value.horizontal && value.horizontal.size && value.horizontal.size < 1) {
value.horizontal.size = 0;
}
setting.set(value);
}
}
_updateVersionFrom7To8() {
}
_updateVersionFrom8To9() {
const settingNames = ['skipStackFramesPattern', 'workspaceFolderExcludePattern'];
for (let i = 0; i < settingNames.length; ++i) {
const setting = Settings.instance().createSetting(settingNames[i], '');
let value = setting.get();
if (!value) {
return;
}
if (typeof value === 'string') {
value = [value];
}
for (let j = 0; j < value.length; ++j) {
if (typeof value[j] === 'string') {
value[j] = {pattern: value[j]};
}
}
setting.set(value);
}
}
_updateVersionFrom9To10() {
// This one is localStorage specific, which is fine.
if (!window.localStorage) {
return;
}
for (const key in window.localStorage) {
if (key.startsWith('revision-history')) {
window.localStorage.removeItem(key);
}
}
}
_updateVersionFrom10To11() {
const oldSettingName = 'customDevicePresets';
const newSettingName = 'customEmulatedDeviceList';
const oldSetting = Settings.instance().createSetting(oldSettingName, undefined);
const list = oldSetting.get();
if (!Array.isArray(list)) {
return;
}
const newList = [];
for (let i = 0; i < list.length; ++i) {
const value = list[i];
/** @type {!Object<string,*>} */
const device = {};
device['title'] = value['title'];
device['type'] = 'unknown';
device['user-agent'] = value['userAgent'];
device['capabilities'] = [];
if (value['touch']) {
device['capabilities'].push('touch');
}
if (value['mobile']) {
device['capabilities'].push('mobile');
}
device['screen'] = {};
device['screen']['vertical'] = {width: value['width'], height: value['height']};
device['screen']['horizontal'] = {width: value['height'], height: value['width']};
device['screen']['device-pixel-ratio'] = value['deviceScaleFactor'];
device['modes'] = [];
device['show-by-default'] = true;
device['show'] = 'Default';
newList.push(device);
}
if (newList.length) {
Settings.instance().createSetting(newSettingName, []).set(newList);
}
oldSetting.remove();
}
_updateVersionFrom11To12() {
this._migrateSettingsFromLocalStorage();
}
_updateVersionFrom12To13() {
this._migrateSettingsFromLocalStorage();
Settings.instance().createSetting('timelineOverviewMode', '').remove();
}
_updateVersionFrom13To14() {
const defaultValue = {'throughput': -1, 'latency': 0};
Settings.instance().createSetting('networkConditions', defaultValue).set(defaultValue);
}
_updateVersionFrom14To15() {
const setting = Settings.instance().createLocalSetting('workspaceExcludedFolders', {});
const oldValue = setting.get();
/** @type {!Object<string,!Array<string>>} */
const newValue = {};
for (const fileSystemPath in oldValue) {
newValue[fileSystemPath] = [];
for (const entry of oldValue[fileSystemPath]) {
newValue[fileSystemPath].push(entry.path);
}
}
setting.set(newValue);
}
_updateVersionFrom15To16() {
const setting = Settings.instance().createSetting('InspectorView.panelOrder', {});
const tabOrders = setting.get();
for (const key of Object.keys(tabOrders)) {
tabOrders[key] = (tabOrders[key] + 1) * 10;
}
setting.set(tabOrders);
}
_updateVersionFrom16To17() {
const setting = Settings.instance().createSetting('networkConditionsCustomProfiles', []);
const oldValue = setting.get();
const newValue = [];
if (Array.isArray(oldValue)) {
for (const preset of oldValue) {
if (typeof preset.title === 'string' && typeof preset.value === 'object' &&
typeof preset.value.throughput === 'number' && typeof preset.value.latency === 'number') {
newValue.push({
title: preset.title,
value:
{download: preset.value.throughput, upload: preset.value.throughput, latency: preset.value.latency}
});
}
}
}
setting.set(newValue);
}
_updateVersionFrom17To18() {
const setting = Settings.instance().createLocalSetting('workspaceExcludedFolders', {});
const oldValue = setting.get();
/** @type {!Object<string,string>} */
const newValue = {};
for (const oldKey in oldValue) {
let newKey = oldKey.replace(/\\/g, '/');
if (!newKey.startsWith('file://')) {
if (newKey.startsWith('/')) {
newKey = 'file://' + newKey;
} else {
newKey = 'file:///' + newKey;
}
}
newValue[newKey] = oldValue[oldKey];
}
setting.set(newValue);
}
_updateVersionFrom18To19() {
const defaultColumns = {status: true, type: true, initiator: true, size: true, time: true};
const visibleColumnSettings = Settings.instance().createSetting('networkLogColumnsVisibility', defaultColumns);
const visibleColumns = visibleColumnSettings.get();
visibleColumns.name = true;
visibleColumns.timeline = true;
/** @type {!Object<string,{visible: number}>} */
const configs = {};
for (const columnId in visibleColumns) {
if (!visibleColumns.hasOwnProperty(columnId)) {
continue;
}
configs[columnId.toLowerCase()] = {visible: visibleColumns[columnId]};
}
const newSetting = Settings.instance().createSetting('networkLogColumns', {});
newSetting.set(configs);
visibleColumnSettings.remove();
}
_updateVersionFrom19To20() {
const oldSetting = Settings.instance().createSetting('InspectorView.panelOrder', {});
const newSetting = Settings.instance().createSetting('panel-tabOrder', {});
newSetting.set(oldSetting.get());
oldSetting.remove();
}
_updateVersionFrom20To21() {
const networkColumns = Settings.instance().createSetting('networkLogColumns', {});
const columns = /** @type {!Object<string,string>} */ (networkColumns.get());
delete columns['timeline'];
delete columns['waterfall'];
networkColumns.set(columns);
}
_updateVersionFrom21To22() {
const breakpointsSetting = Settings.instance().createLocalSetting('breakpoints', []);
const breakpoints = breakpointsSetting.get();
for (const breakpoint of breakpoints) {
breakpoint['url'] = breakpoint['sourceFileId'];
delete breakpoint['sourceFileId'];
}
breakpointsSetting.set(breakpoints);
}
_updateVersionFrom22To23() {
// This update is no-op.
}
_updateVersionFrom23To24() {
const oldSetting = Settings.instance().createSetting('searchInContentScripts', false);
const newSetting = Settings.instance().createSetting('searchInAnonymousAndContentScripts', false);
newSetting.set(oldSetting.get());
oldSetting.remove();
}
_updateVersionFrom24To25() {
const defaultColumns = {status: true, type: true, initiator: true, size: true, time: true};
const networkLogColumnsSetting = Settings.instance().createSetting('networkLogColumns', defaultColumns);
const columns = networkLogColumnsSetting.get();
delete columns.product;
networkLogColumnsSetting.set(columns);
}
_updateVersionFrom25To26() {
const oldSetting = Settings.instance().createSetting('messageURLFilters', {});
const urls = Object.keys(oldSetting.get());
const textFilter = urls.map(url => `-url:${url}`).join(' ');
if (textFilter) {
const textFilterSetting = Settings.instance().createSetting('console.textFilter', '');
const suffix = textFilterSetting.get() ? ` ${textFilterSetting.get()}` : '';
textFilterSetting.set(`${textFilter}${suffix}`);
}
oldSetting.remove();
}
_updateVersionFrom26To27() {
/**
* @param {string} settingName
* @param {string} from
* @param {string} to
*/
function renameKeyInObjectSetting(settingName, from, to) {
const setting = Settings.instance().createSetting(settingName, {});
const value = setting.get();
if (from in value) {
value[to] = value[from];
delete value[from];
setting.set(value);
}
}
/**
* @param {string} settingName
* @param {string} from
* @param {string} to
*/
function renameInStringSetting(settingName, from, to) {
const setting = Settings.instance().createSetting(settingName, '');
const value = setting.get();
if (value === from) {
setting.set(to);
}
}
renameKeyInObjectSetting('panel-tabOrder', 'audits2', 'audits');
renameKeyInObjectSetting('panel-closeableTabs', 'audits2', 'audits');
renameInStringSetting('panel-selectedTab', 'audits2', 'audits');
}
_updateVersionFrom27To28() {
const setting = Settings.instance().createSetting('uiTheme', 'systemPreferred');
if (setting.get() === 'default') {
setting.set('systemPreferred');
}
}
_updateVersionFrom28To29() {
/**
* @param {string} settingName
* @param {string} from
* @param {string} to
*/
function renameKeyInObjectSetting(settingName, from, to) {
const setting = Settings.instance().createSetting(settingName, {});
const value = setting.get();
if (from in value) {
value[to] = value[from];
delete value[from];
setting.set(value);
}
}
/**
* @param {string} settingName
* @param {string} from
* @param {string} to
*/
function renameInStringSetting(settingName, from, to) {
const setting = Settings.instance().createSetting(settingName, '');
const value = setting.get();
if (value === from) {
setting.set(to);
}
}
renameKeyInObjectSetting('panel-tabOrder', 'audits', 'lighthouse');
renameKeyInObjectSetting('panel-closeableTabs', 'audits', 'lighthouse');
renameInStringSetting('panel-selectedTab', 'audits', 'lighthouse');
}
_updateVersionFrom29To30() {
// Create new location agnostic setting
const closeableTabSetting = Settings.instance().createSetting('closeableTabs', {});
// Read current settings
const panelCloseableTabSetting = Settings.instance().createSetting('panel-closeableTabs', {});
const drawerCloseableTabSetting = Settings.instance().createSetting('drawer-view-closeableTabs', {});
const openTabsInPanel = panelCloseableTabSetting.get();
const openTabsInDrawer = panelCloseableTabSetting.get();
// Set value of new setting
const newValue = Object.assign(openTabsInDrawer, openTabsInPanel);
closeableTabSetting.set(newValue);
// Remove old settings
panelCloseableTabSetting.remove();
drawerCloseableTabSetting.remove();
}
_migrateSettingsFromLocalStorage() {
// This step migrates all the settings except for the ones below into the browser profile.
const localSettings = new Set([
'advancedSearchConfig', 'breakpoints', 'consoleHistory', 'domBreakpoints', 'eventListenerBreakpoints',
'fileSystemMapping', 'lastSelectedSourcesSidebarPaneTab', 'previouslyViewedFiles', 'savedURLs',
'watchExpressions', 'workspaceExcludedFolders', 'xhrBreakpoints'
]);
if (!window.localStorage) {
return;
}
for (const key in window.localStorage) {
if (localSettings.has(key)) {
continue;
}
const value = window.localStorage[key];
window.localStorage.removeItem(key);
Settings.instance()._globalStorage.set(key, value);
}
}
/**
* @param {!Setting<*>} breakpointsSetting
* @param {number} maxBreakpointsCount
*/
_clearBreakpointsWhenTooMany(breakpointsSetting, maxBreakpointsCount) {
// If there are too many breakpoints in a storage, it is likely due to a recent bug that caused
// periodical breakpoints duplication leading to inspector slowness.
if (breakpointsSetting.get().length > maxBreakpointsCount) {
breakpointsSetting.set([]);
}
}
}
/**
* @enum {symbol}
*/
export const SettingStorageType = {
Global: Symbol('Global'),
Local: Symbol('Local'),
Session: Symbol('Session')
};
/**
* @param {string} settingName
* @return {!Setting<*>}
*/
export function moduleSetting(settingName) {
return Settings.instance().moduleSetting(settingName);
}
/**
* @param {string} settingName
* @return {!Setting<*>}
*/
export function settingForTest(settingName) {
return Settings.instance().settingForTest(settingName);
}
/**
* @param {!Color} color
* @return {!Format}
*/
export function detectColorFormat(color) {
const cf = Format;
let format;
const formatSetting = Settings.instance().moduleSetting('colorFormat').get();
if (formatSetting === cf.Original) {
format = cf.Original;
} else if (formatSetting === cf.RGB) {
format = cf.RGB;
} else if (formatSetting === cf.HSL) {
format = cf.HSL;
} else if (formatSetting === cf.HEX) {
format = color.detectHEXFormat();
} else {
format = cf.RGB;
}
return format;
}
export {
getRegisteredSettings,
registerSettingExtension,
RegExpSettingItem,
SettingCategory,
SettingCategoryObject,
SettingExtensionOption,
SettingRegistration,
SettingType,
SettingTypeObject
};
/**
* @typedef {{
* value: (boolean|string),
* title: string,
* text: (string|undefined),
* raw: (boolean | undefined),
* }}
*/
// @ts-ignore typedef
export let SimpleSettingOption;