blob: be06593c7ae00f3bc26cdce38dcb746b9a91a5ae [file] [log] [blame]
// Copyright 2015 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.
/**
* @constructor
* @param {function()} updateCallback
* @implements {WebInspector.TargetManager.Observer}
*/
WebInspector.DeviceModeModel = function(updateCallback)
{
this._updateCallback = updateCallback;
this._screenRect = new WebInspector.Rect(0, 0, 1, 1);
this._visiblePageRect = new WebInspector.Rect(0, 0, 1, 1);
this._availableSize = new Size(1, 1);
this._deviceMetricsThrottler = new WebInspector.Throttler(0);
this._appliedDeviceSize = new Size(1, 1);
this._currentDeviceScaleFactor = window.devicePixelRatio;
this._appliedDeviceScaleFactor = 0;
// Zero means "fit".
this._scaleSetting = WebInspector.settings.createSetting("emulation.deviceScale", 1);
this._scaleSetting.addChangeListener(this._scaleSettingChanged, this);
this._widthSetting = WebInspector.settings.createSetting("emulation.deviceWidth", 400);
this._widthSetting.addChangeListener(this._widthSettingChanged, this);
this._heightSetting = WebInspector.settings.createSetting("emulation.deviceHeight", 700);
this._heightSetting.addChangeListener(this._heightSettingChanged, this);
this._uaSetting = WebInspector.settings.createSetting("emulation.deviceUA", WebInspector.DeviceModeModel.UA.Mobile);
this._uaSetting.addChangeListener(this._uaSettingChanged, this);
this._deviceScaleFactorSetting = WebInspector.settings.createSetting("emulation.deviceScaleFactor", 0);
this._deviceScaleFactorSetting.addChangeListener(this._deviceScaleFactorSettingChanged, this);
/** @type {!WebInspector.DeviceModeModel.Type} */
this._type = WebInspector.DeviceModeModel.Type.None;
/** @type {?WebInspector.EmulatedDevice} */
this._device = null;
/** @type {?WebInspector.EmulatedDevice.Mode} */
this._mode = null;
/** @type {boolean} */
this._touchEnabled = false;
/** @type {string} */
this._touchConfiguration = "";
/** @type {string} */
this._screenOrientation = "";
/** @type {number} */
this._fixedScale = 0;
/** @type {?WebInspector.Target} */
this._target = null;
WebInspector.targetManager.observeTargets(this, WebInspector.Target.Type.Page);
}
/** @enum {string} */
WebInspector.DeviceModeModel.Type = {
None: "None",
Responsive: "Responsive",
Device: "Device"
}
/** @enum {string} */
WebInspector.DeviceModeModel.UA = {
Mobile: "Mobile",
Desktop: "Desktop",
DesktopTouch: "DesktopTouch"
}
WebInspector.DeviceModeModel.MaxDeviceSize = 10000;
/**
* @param {string} value
* @return {string}
*/
WebInspector.DeviceModeModel.deviceSizeValidator = function(value)
{
if (/^[\d]+$/.test(value) && value > 0 && value <= WebInspector.DeviceModeModel.MaxDeviceSize)
return "";
return WebInspector.UIString("Value must be positive integer");
}
WebInspector.DeviceModeModel._touchEventsScriptIdSymbol = Symbol("DeviceModeModel.touchEventsScriptIdSymbol");
WebInspector.DeviceModeModel._defaultMobileUserAgent = "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.76 Mobile Safari/537.36";
WebInspector.DeviceModeModel._defaultMobileScaleFactor = 2;
WebInspector.DeviceModeModel.prototype = {
/**
* @param {!Size} size
*/
setAvailableSize: function(size)
{
this._availableSize = size;
this._calculateAndEmulate(false);
},
/**
* @param {!WebInspector.DeviceModeModel.Type} type
* @param {?WebInspector.EmulatedDevice} device
* @param {?WebInspector.EmulatedDevice.Mode} mode
*/
emulate: function(type, device, mode)
{
this._type = type;
if (type === WebInspector.DeviceModeModel.Type.Device) {
console.assert(device && mode, "Must pass device and mode for device emulation");
this._device = device;
this._mode = mode;
} else {
this._device = null;
this._mode = null;
}
this._calculateAndEmulate(true);
},
/**
* @return {?WebInspector.EmulatedDevice}
*/
device: function()
{
return this._device;
},
/**
* @return {?WebInspector.EmulatedDevice.Mode}
*/
mode: function()
{
return this._mode;
},
/**
* @return {!WebInspector.DeviceModeModel.Type}
*/
type: function()
{
return this._type;
},
/**
* @return {string}
*/
screenImage: function()
{
return (this._device && this._mode) ? this._device.modeImage(this._mode) : "";
},
/**
* @return {!WebInspector.Rect}
*/
screenRect: function()
{
return this._screenRect;
},
/**
* @return {!WebInspector.Rect}
*/
visiblePageRect: function()
{
return this._visiblePageRect;
},
/**
* @return {number}
*/
scale: function()
{
return this._scale;
},
suspendScaleChanges: function()
{
++this._fixedScale;
},
resumeScaleChanges: function()
{
if (!--this._fixedScale)
this._calculateAndEmulate(false);
},
/**
* @return {!Size}
*/
appliedDeviceSize: function()
{
return this._appliedDeviceSize;
},
/**
* @return {number}
*/
appliedDeviceScaleFactor: function()
{
return this._appliedDeviceScaleFactor;
},
/**
* @return {!WebInspector.Setting}
*/
scaleSetting: function()
{
return this._scaleSetting;
},
/**
* @return {!WebInspector.Setting}
*/
widthSetting: function()
{
return this._widthSetting;
},
/**
* @return {!WebInspector.Setting}
*/
heightSetting: function()
{
return this._heightSetting;
},
/**
* @return {!WebInspector.Setting}
*/
uaSetting: function()
{
return this._uaSetting;
},
/**
* @return {!WebInspector.Setting}
*/
deviceScaleFactorSetting: function()
{
return this._deviceScaleFactorSetting;
},
/**
* @return {number}
*/
defaultDeviceScaleFactor: function()
{
if (this._type === WebInspector.DeviceModeModel.Type.Responsive)
return this._uaSetting.get() === WebInspector.DeviceModeModel.UA.Mobile ? WebInspector.DeviceModeModel._defaultMobileScaleFactor : this._currentDeviceScaleFactor;
else if (this._type === WebInspector.DeviceModeModel.Type.Device)
return this._device.deviceScaleFactor;
else
return this._currentDeviceScaleFactor;
},
reset: function()
{
this._deviceScaleFactorSetting.set(0);
this._scaleSetting.set(0);
this._widthSetting.set(400);
this._heightSetting.set(700);
this._uaSetting.set(WebInspector.DeviceModeModel.UA.Mobile);
},
/**
* @override
* @param {!WebInspector.Target} target
*/
targetAdded: function(target)
{
if (!this._target) {
this._target = target;
var domModel = WebInspector.DOMModel.fromTarget(this._target);
domModel.addEventListener(WebInspector.DOMModel.Events.InspectModeWillBeToggled, this._inspectModeWillBeToggled, this);
}
},
/**
* @param {!WebInspector.Event} event
*/
_inspectModeWillBeToggled: function(event)
{
var inspectModeEnabled = /** @type {boolean} */ (event.data);
if (inspectModeEnabled) {
this._applyTouch(false, false);
return;
}
if (this._type === WebInspector.DeviceModeModel.Type.Device)
this._applyTouch(this._device.touch(), this._device.mobile());
else if (this._type === WebInspector.DeviceModeModel.Type.None)
this._applyTouch(false, false);
else if (this._type === WebInspector.DeviceModeModel.Type.Responsive)
this._applyTouch(this._uaSetting.get() !== WebInspector.DeviceModeModel.UA.Desktop, this._uaSetting.get() === WebInspector.DeviceModeModel.UA.Mobile);
},
/**
* @override
* @param {!WebInspector.Target} target
*/
targetRemoved: function(target)
{
if (this._target === target)
this._target = null;
},
_scaleSettingChanged: function()
{
this._calculateAndEmulate(true);
},
_widthSettingChanged: function()
{
this._calculateAndEmulate(false);
},
_heightSettingChanged: function()
{
this._calculateAndEmulate(false);
},
_uaSettingChanged: function()
{
this._calculateAndEmulate(true);
},
_deviceScaleFactorSettingChanged: function()
{
this._calculateAndEmulate(false);
},
/**
* @param {boolean} resetScrollAndPageScale
*/
_calculateAndEmulate: function(resetScrollAndPageScale)
{
if (this._type === WebInspector.DeviceModeModel.Type.Device) {
var orientation = this._device.orientationByName(this._mode.orientation);
var screenWidth = orientation.width;
var screenHeight = orientation.height;
var scale = this._calculateScale(screenWidth, screenHeight);
this._applyDeviceMetrics(new Size(screenWidth, screenHeight), this._mode.insets, scale, this._device.deviceScaleFactor, this._device.mobile(), resetScrollAndPageScale);
this._applyUserAgent(this._device.userAgent);
this._applyTouch(this._device.touch(), this._device.mobile());
this._applyScreenOrientation(this._mode.orientation == WebInspector.EmulatedDevice.Horizontal ? "landscapePrimary" : "portraitPrimary");
} else if (this._type === WebInspector.DeviceModeModel.Type.None) {
this._applyDeviceMetrics(this._availableSize, new Insets(0, 0, 0, 0), 1, 0, false, resetScrollAndPageScale);
this._applyUserAgent("");
this._applyTouch(false, false);
this._applyScreenOrientation("");
} else if (this._type === WebInspector.DeviceModeModel.Type.Responsive) {
var screenWidth = this._widthSetting.get();
var screenHeight = this._heightSetting.get();
var scale = this._calculateScale(screenWidth, screenHeight);
var mobile = this._uaSetting.get() === WebInspector.DeviceModeModel.UA.Mobile;
var defaultDeviceScaleFactor = mobile ? WebInspector.DeviceModeModel._defaultMobileScaleFactor : 0;
this._applyDeviceMetrics(new Size(screenWidth, screenHeight), new Insets(0, 0, 0, 0), scale, this._deviceScaleFactorSetting.get() || defaultDeviceScaleFactor, mobile, resetScrollAndPageScale);
this._applyUserAgent(mobile ? WebInspector.DeviceModeModel._defaultMobileUserAgent : "");
this._applyTouch(this._uaSetting.get() !== WebInspector.DeviceModeModel.UA.Desktop, mobile);
this._applyScreenOrientation(screenHeight >= screenWidth ? "portraitPrimary" : "landscapePrimary");
}
this._updateCallback.call(null);
},
/**
* @param {number} screenWidth
* @param {number} screenHeight
* @return {number}
*/
_calculateScale: function(screenWidth, screenHeight)
{
var scale = this._scaleSetting.get();
if (!scale) {
if (this._fixedScale) {
scale = this._scale;
} else {
scale = 1;
while (this._availableSize.width < screenWidth * scale || this._availableSize.height < screenHeight * scale)
scale *= 0.8;
}
}
return scale;
},
/**
* @param {string} userAgent
*/
_applyUserAgent: function(userAgent)
{
WebInspector.multitargetNetworkManager.setUserAgentOverride(userAgent);
},
/**
* @param {!Size} screenSize
* @param {!Insets} insets
* @param {number} scale
* @param {number} deviceScaleFactor
* @param {boolean} mobile
* @param {boolean} resetScrollAndPageScale
*/
_applyDeviceMetrics: function(screenSize, insets, scale, deviceScaleFactor, mobile, resetScrollAndPageScale)
{
screenSize.width = Math.max(1, Math.floor(screenSize.width));
screenSize.height = Math.max(1, Math.floor(screenSize.height));
var pageWidth = screenSize.width - insets.left - insets.right;
var pageHeight = screenSize.height - insets.top - insets.bottom;
var positionX = insets.left;
var positionY = insets.top;
this._appliedDeviceSize = screenSize;
this._screenRect = new WebInspector.Rect(
Math.max(0, (this._availableSize.width - screenSize.width * scale) / 2),
this._type === WebInspector.DeviceModeModel.Type.Device ? Math.max(0, (this._availableSize.height - screenSize.height * scale) / 2) : 0,
screenSize.width * scale,
screenSize.height * scale);
this._visiblePageRect = new WebInspector.Rect(
positionX * scale,
positionY * scale,
Math.min(pageWidth * scale, this._availableSize.width - this._screenRect.left - positionX * scale),
Math.min(pageHeight * scale, this._availableSize.height - this._screenRect.top - positionY * scale));
this._scale = scale;
this._appliedDeviceScaleFactor = deviceScaleFactor;
if (scale === 1 && this._availableSize.width >= screenSize.width && this._availableSize.height >= screenSize.height) {
// When we have enough space, no page size override is required. This will speed things up and remove lag.
pageWidth = 0;
pageHeight = 0;
}
this._deviceMetricsThrottler.schedule(setDeviceMetricsOverride.bind(this));
/**
* @this {WebInspector.DeviceModeModel}
* @return {!Promise.<?>}
*/
function setDeviceMetricsOverride()
{
if (!this._target)
return Promise.resolve();
var clear = !pageWidth && !pageHeight && !mobile && !deviceScaleFactor && scale === 1;
var setDevicePromise = clear ?
this._target.emulationAgent().clearDeviceMetricsOverride(this._deviceMetricsOverrideAppliedForTest.bind(this)) :
this._target.emulationAgent().setDeviceMetricsOverride(pageWidth, pageHeight, deviceScaleFactor, mobile, false, scale, 0, 0, screenSize.width, screenSize.height, positionX, positionY, this._deviceMetricsOverrideAppliedForTest.bind(this));
var allPromises = [ setDevicePromise ];
if (resetScrollAndPageScale)
allPromises.push(this._target.emulationAgent().resetScrollAndPageScaleFactor());
return Promise.all(allPromises);
}
},
_deviceMetricsOverrideAppliedForTest: function()
{
// Used for sniffing in tests.
},
_applyTouch: function(touchEnabled, mobile)
{
var configuration = mobile ? "mobile" : "desktop";
if (!this._target || (this._touchEnabled === touchEnabled && this._touchConfiguration === configuration))
return;
var target = this._target;
/**
* @suppressGlobalPropertiesCheck
*/
const injectedFunction = function() {
const touchEvents = ["ontouchstart", "ontouchend", "ontouchmove", "ontouchcancel"];
var recepients = [window.__proto__, document.__proto__];
for (var i = 0; i < touchEvents.length; ++i) {
for (var j = 0; j < recepients.length; ++j) {
if (!(touchEvents[i] in recepients[j]))
Object.defineProperty(recepients[j], touchEvents[i], { value: null, writable: true, configurable: true, enumerable: true });
}
}
};
var symbol = WebInspector.DeviceModeModel._touchEventsScriptIdSymbol;
if (typeof target[symbol] !== "undefined") {
target.pageAgent().removeScriptToEvaluateOnLoad(target[symbol]);
delete target[symbol];
}
if (touchEnabled)
target.pageAgent().addScriptToEvaluateOnLoad("(" + injectedFunction.toString() + ")()", scriptAddedCallback);
/**
* @param {?Protocol.Error} error
* @param {string} scriptId
*/
function scriptAddedCallback(error, scriptId)
{
if (error)
delete target[symbol];
else
target[symbol] = scriptId;
}
target.emulationAgent().setTouchEmulationEnabled(touchEnabled, configuration);
this._touchEnabled = touchEnabled;
this._touchConfiguration = configuration;
},
/**
* @param {string} orientation
*/
_applyScreenOrientation: function(orientation)
{
if (!this._target || orientation === this._screenOrientation)
return;
this._screenOrientation = orientation;
if (!this._screenOrientation)
this._target.screenOrientationAgent().clearScreenOrientationOverride();
else
this._target.screenOrientationAgent().setScreenOrientationOverride(this._screenOrientation === "landscapePrimary" ? 90 : 0, /** @type {!ScreenOrientationAgent.OrientationType} */ (this._screenOrientation));
}
}