| // 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; |