blob: 291882320cb853a6828a0f6ac7d1439613ae629b [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @unrestricted
*/
export class EmulatedDevice {
constructor() {
/** @type {string} */
this.title = '';
/** @type {string} */
this.type = Type.Unknown;
/** @type {!Emulation.EmulatedDevice.Orientation} */
this.vertical = {width: 0, height: 0, outlineInsets: null, outlineImage: null};
/** @type {!Emulation.EmulatedDevice.Orientation} */
this.horizontal = {width: 0, height: 0, outlineInsets: null, outlineImage: null};
/** @type {number} */
this.deviceScaleFactor = 1;
/** @type {!Array.<string>} */
this.capabilities = [Capability.Touch, Capability.Mobile];
/** @type {string} */
this.userAgent = '';
/** @type {!Array.<!Emulation.EmulatedDevice.Mode>} */
this.modes = [];
/** @type {string} */
this._show = _Show.Default;
/** @type {boolean} */
this._showByDefault = true;
/** @type {?Root.Runtime.Extension} */
this._extension = null;
}
/**
* @param {*} json
* @return {?EmulatedDevice}
*/
static fromJSONV1(json) {
try {
/**
* @param {*} object
* @param {string} key
* @param {string} type
* @param {*=} defaultValue
* @return {*}
*/
function parseValue(object, key, type, defaultValue) {
if (typeof object !== 'object' || object === null || !object.hasOwnProperty(key)) {
if (typeof defaultValue !== 'undefined') {
return defaultValue;
}
throw new Error('Emulated device is missing required property \'' + key + '\'');
}
const value = object[key];
if (typeof value !== type || value === null) {
throw new Error('Emulated device property \'' + key + '\' has wrong type \'' + typeof value + '\'');
}
return value;
}
/**
* @param {*} object
* @param {string} key
* @return {number}
*/
function parseIntValue(object, key) {
const value = /** @type {number} */ (parseValue(object, key, 'number'));
if (value !== Math.abs(value)) {
throw new Error('Emulated device value \'' + key + '\' must be integer');
}
return value;
}
/**
* @param {*} json
* @return {!UI.Insets}
*/
function parseInsets(json) {
return new UI.Insets(
parseIntValue(json, 'left'), parseIntValue(json, 'top'), parseIntValue(json, 'right'),
parseIntValue(json, 'bottom'));
}
/**
* @param {*} json
* @return {!EmulatedDevice.Orientation}
*/
function parseOrientation(json) {
const result = {};
result.width = parseIntValue(json, 'width');
if (result.width < 0 || result.width > Emulation.DeviceModeModel.MaxDeviceSize ||
result.width < Emulation.DeviceModeModel.MinDeviceSize) {
throw new Error('Emulated device has wrong width: ' + result.width);
}
result.height = parseIntValue(json, 'height');
if (result.height < 0 || result.height > Emulation.DeviceModeModel.MaxDeviceSize ||
result.height < Emulation.DeviceModeModel.MinDeviceSize) {
throw new Error('Emulated device has wrong height: ' + result.height);
}
const outlineInsets = parseValue(json['outline'], 'insets', 'object', null);
if (outlineInsets) {
result.outlineInsets = parseInsets(outlineInsets);
if (result.outlineInsets.left < 0 || result.outlineInsets.top < 0) {
throw new Error('Emulated device has wrong outline insets');
}
result.outlineImage = /** @type {string} */ (parseValue(json['outline'], 'image', 'string'));
}
return /** @type {!EmulatedDevice.Orientation} */ (result);
}
const result = new EmulatedDevice();
result.title = /** @type {string} */ (parseValue(json, 'title', 'string'));
result.type = /** @type {string} */ (parseValue(json, 'type', 'string'));
const rawUserAgent = /** @type {string} */ (parseValue(json, 'user-agent', 'string'));
result.userAgent = SDK.MultitargetNetworkManager.patchUserAgentWithChromeVersion(rawUserAgent);
const capabilities = parseValue(json, 'capabilities', 'object', []);
if (!Array.isArray(capabilities)) {
throw new Error('Emulated device capabilities must be an array');
}
result.capabilities = [];
for (let i = 0; i < capabilities.length; ++i) {
if (typeof capabilities[i] !== 'string') {
throw new Error('Emulated device capability must be a string');
}
result.capabilities.push(capabilities[i]);
}
result.deviceScaleFactor = /** @type {number} */ (parseValue(json['screen'], 'device-pixel-ratio', 'number'));
if (result.deviceScaleFactor < 0 || result.deviceScaleFactor > 100) {
throw new Error('Emulated device has wrong deviceScaleFactor: ' + result.deviceScaleFactor);
}
result.vertical = parseOrientation(parseValue(json['screen'], 'vertical', 'object'));
result.horizontal = parseOrientation(parseValue(json['screen'], 'horizontal', 'object'));
const modes = parseValue(json, 'modes', 'object', []);
if (!Array.isArray(modes)) {
throw new Error('Emulated device modes must be an array');
}
result.modes = [];
for (let i = 0; i < modes.length; ++i) {
const mode = {};
mode.title = /** @type {string} */ (parseValue(modes[i], 'title', 'string'));
mode.orientation = /** @type {string} */ (parseValue(modes[i], 'orientation', 'string'));
if (mode.orientation !== Vertical && mode.orientation !== Horizontal) {
throw new Error('Emulated device mode has wrong orientation \'' + mode.orientation + '\'');
}
const orientation = result.orientationByName(mode.orientation);
mode.insets = parseInsets(parseValue(modes[i], 'insets', 'object'));
if (mode.insets.top < 0 || mode.insets.left < 0 || mode.insets.right < 0 || mode.insets.bottom < 0 ||
mode.insets.top + mode.insets.bottom > orientation.height ||
mode.insets.left + mode.insets.right > orientation.width) {
throw new Error('Emulated device mode \'' + mode.title + '\'has wrong mode insets');
}
mode.image = /** @type {string} */ (parseValue(modes[i], 'image', 'string', null));
result.modes.push(mode);
}
result._showByDefault = /** @type {boolean} */ (parseValue(json, 'show-by-default', 'boolean', undefined));
result._show =
/** @type {string} */ (parseValue(json, 'show', 'string', _Show.Default));
return result;
} catch (e) {
return null;
}
}
/**
* @param {!EmulatedDevice} device1
* @param {!EmulatedDevice} device2
* @return {number}
*/
static deviceComparator(device1, device2) {
const order1 = (device1._extension && device1._extension.descriptor()['order']) || -1;
const order2 = (device2._extension && device2._extension.descriptor()['order']) || -1;
if (order1 > order2) {
return 1;
}
if (order2 > order1) {
return -1;
}
return device1.title < device2.title ? -1 : (device1.title > device2.title ? 1 : 0);
}
/**
* @return {?Root.Runtime.Extension}
*/
extension() {
return this._extension;
}
/**
* @param {?Root.Runtime.Extension} extension
*/
setExtension(extension) {
this._extension = extension;
}
/**
* @param {string} orientation
* @return {!Array.<!EmulatedDevice.Mode>}
*/
modesForOrientation(orientation) {
const result = [];
for (let index = 0; index < this.modes.length; index++) {
if (this.modes[index].orientation === orientation) {
result.push(this.modes[index]);
}
}
return result;
}
/**
* @return {*}
*/
_toJSON() {
const json = {};
json['title'] = this.title;
json['type'] = this.type;
json['user-agent'] = this.userAgent;
json['capabilities'] = this.capabilities;
json['screen'] = {};
json['screen']['device-pixel-ratio'] = this.deviceScaleFactor;
json['screen']['vertical'] = this._orientationToJSON(this.vertical);
json['screen']['horizontal'] = this._orientationToJSON(this.horizontal);
json['modes'] = [];
for (let i = 0; i < this.modes.length; ++i) {
const mode = {};
mode['title'] = this.modes[i].title;
mode['orientation'] = this.modes[i].orientation;
mode['insets'] = {};
mode['insets']['left'] = this.modes[i].insets.left;
mode['insets']['top'] = this.modes[i].insets.top;
mode['insets']['right'] = this.modes[i].insets.right;
mode['insets']['bottom'] = this.modes[i].insets.bottom;
if (this.modes[i].image) {
mode['image'] = this.modes[i].image;
}
json['modes'].push(mode);
}
json['show-by-default'] = this._showByDefault;
json['show'] = this._show;
return json;
}
/**
* @param {!EmulatedDevice.Orientation} orientation
* @return {*}
*/
_orientationToJSON(orientation) {
const json = {};
json['width'] = orientation.width;
json['height'] = orientation.height;
if (orientation.outlineInsets) {
json['outline'] = {};
json['outline']['insets'] = {};
json['outline']['insets']['left'] = orientation.outlineInsets.left;
json['outline']['insets']['top'] = orientation.outlineInsets.top;
json['outline']['insets']['right'] = orientation.outlineInsets.right;
json['outline']['insets']['bottom'] = orientation.outlineInsets.bottom;
json['outline']['image'] = orientation.outlineImage;
}
return json;
}
/**
* @param {!EmulatedDevice.Mode} mode
* @return {string}
*/
modeImage(mode) {
if (!mode.image) {
return '';
}
if (!this._extension) {
return mode.image;
}
return this._extension.module().substituteURL(mode.image);
}
/**
* @param {!EmulatedDevice.Mode} mode
* @return {string}
*/
outlineImage(mode) {
const orientation = this.orientationByName(mode.orientation);
if (!orientation.outlineImage) {
return '';
}
if (!this._extension) {
return orientation.outlineImage;
}
return this._extension.module().substituteURL(orientation.outlineImage);
}
/**
* @param {string} name
* @return {!EmulatedDevice.Orientation}
*/
orientationByName(name) {
return name === Vertical ? this.vertical : this.horizontal;
}
/**
* @return {boolean}
*/
show() {
if (this._show === _Show.Default) {
return this._showByDefault;
}
return this._show === _Show.Always;
}
/**
* @param {boolean} show
*/
setShow(show) {
this._show = show ? _Show.Always : _Show.Never;
}
/**
* @param {!EmulatedDevice} other
*/
copyShowFrom(other) {
this._show = other._show;
}
/**
* @return {boolean}
*/
touch() {
return this.capabilities.indexOf(Capability.Touch) !== -1;
}
/**
* @return {boolean}
*/
mobile() {
return this.capabilities.indexOf(Capability.Mobile) !== -1;
}
}
export const Horizontal = 'horizontal';
export const Vertical = 'vertical';
export const Type = {
Phone: 'phone',
Tablet: 'tablet',
Notebook: 'notebook',
Desktop: 'desktop',
Unknown: 'unknown'
};
export const Capability = {
Touch: 'touch',
Mobile: 'mobile'
};
export const _Show = {
Always: 'Always',
Default: 'Default',
Never: 'Never'
};
/**
* @unrestricted
*/
export class EmulatedDevicesList extends Common.Object {
constructor() {
super();
/** @type {!Common.Setting} */
this._standardSetting = Common.settings.createSetting('standardEmulatedDeviceList', []);
/** @type {!Array.<!EmulatedDevice>} */
this._standard = [];
this._listFromJSONV1(this._standardSetting.get(), this._standard);
this._updateStandardDevices();
/** @type {!Common.Setting} */
this._customSetting = Common.settings.createSetting('customEmulatedDeviceList', []);
/** @type {!Array.<!EmulatedDevice>} */
this._custom = [];
if (!this._listFromJSONV1(this._customSetting.get(), this._custom)) {
this.saveCustomDevices();
}
}
/**
* @return {!EmulatedDevicesList}
*/
static instance() {
if (!this._instance) {
this._instance = new EmulatedDevicesList();
}
return /** @type {!EmulatedDevicesList} */ (this._instance);
}
_updateStandardDevices() {
const devices = [];
const extensions = self.runtime.extensions('emulated-device');
for (let i = 0; i < extensions.length; ++i) {
const device = EmulatedDevice.fromJSONV1(extensions[i].descriptor()['device']);
device.setExtension(extensions[i]);
devices.push(device);
}
this._copyShowValues(this._standard, devices);
this._standard = devices;
this.saveStandardDevices();
}
/**
* @param {!Array.<*>} jsonArray
* @param {!Array.<!EmulatedDevice>} result
* @return {boolean}
*/
_listFromJSONV1(jsonArray, result) {
if (!Array.isArray(jsonArray)) {
return false;
}
let success = true;
for (let i = 0; i < jsonArray.length; ++i) {
const device = EmulatedDevice.fromJSONV1(jsonArray[i]);
if (device) {
result.push(device);
if (!device.modes.length) {
device.modes.push({title: '', orientation: Horizontal, insets: new UI.Insets(0, 0, 0, 0), image: null});
device.modes.push({title: '', orientation: Vertical, insets: new UI.Insets(0, 0, 0, 0), image: null});
}
} else {
success = false;
}
}
return success;
}
/**
* @return {!Array.<!EmulatedDevice>}
*/
standard() {
return this._standard;
}
/**
* @return {!Array.<!EmulatedDevice>}
*/
custom() {
return this._custom;
}
revealCustomSetting() {
Common.Revealer.reveal(this._customSetting);
}
/**
* @param {!EmulatedDevice} device
*/
addCustomDevice(device) {
this._custom.push(device);
this.saveCustomDevices();
}
/**
* @param {!EmulatedDevice} device
*/
removeCustomDevice(device) {
this._custom.remove(device);
this.saveCustomDevices();
}
saveCustomDevices() {
const json = this._custom.map(/** @param {!EmulatedDevice} device */ function(device) {
return device._toJSON();
});
this._customSetting.set(json);
this.dispatchEventToListeners(Events.CustomDevicesUpdated);
}
saveStandardDevices() {
const json = this._standard.map(/** @param {!EmulatedDevice} device */ function(device) {
return device._toJSON();
});
this._standardSetting.set(json);
this.dispatchEventToListeners(Events.StandardDevicesUpdated);
}
/**
* @param {!Array.<!EmulatedDevice>} from
* @param {!Array.<!EmulatedDevice>} to
*/
_copyShowValues(from, to) {
const deviceById = new Map();
for (let i = 0; i < from.length; ++i) {
deviceById.set(from[i].title, from[i]);
}
for (let i = 0; i < to.length; ++i) {
const title = to[i].title;
if (deviceById.has(title)) {
to[i].copyShowFrom(/** @type {!EmulatedDevice} */ (deviceById.get(title)));
}
}
}
}
/** @enum {symbol} */
export const Events = {
CustomDevicesUpdated: Symbol('CustomDevicesUpdated'),
StandardDevicesUpdated: Symbol('StandardDevicesUpdated')
};
/* Legacy exported object */
self.Emulation = self.Emulation || {};
/* Legacy exported object */
Emulation = Emulation || {};
/**
* @constructor
*/
Emulation.EmulatedDevice = EmulatedDevice;
/** @typedef {!{title: string, orientation: string, insets: !UI.Insets, image: ?string}} */
Emulation.EmulatedDevice.Mode;
/** @typedef {!{width: number, height: number, outlineInsets: ?UI.Insets, outlineImage: ?string}} */
Emulation.EmulatedDevice.Orientation;
Emulation.EmulatedDevice.Horizontal = Horizontal;
Emulation.EmulatedDevice.Vertical = Vertical;
Emulation.EmulatedDevice.Type = Type;
Emulation.EmulatedDevice.Capability = Capability;
Emulation.EmulatedDevice._Show = _Show;
/**
* @constructor
*/
Emulation.EmulatedDevicesList = EmulatedDevicesList;
/** @type {?EmulatedDevicesList} */
Emulation.EmulatedDevicesList._instance;
/** @enum {symbol} */
Emulation.EmulatedDevicesList.Events = Events;