blob: c24f4526b40b7237c5a0d88b0ef7d061b5bd3a60 [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._fitScale = 1;
this._availableSize = new Size(1, 1);
this._deviceMetricsThrottler = new WebInspector.Throttler(0);
this._fitSetting = WebInspector.settings.createSetting("deviceFitWindow", true);
this._fitSetting.addChangeListener(this._fitSettingChanged, this);
this._genericWidthSetting = WebInspector.settings.createSetting("deviceGenericWidth", 0);
this._genericWidthSetting.set(0);
this._genericWidthSetting.addChangeListener(this._genericWidthSettingChanged, this);
/** @type {!WebInspector.DeviceModeModel.Type} */
this._type = WebInspector.DeviceModeModel.Type.Desktop;
/** @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._fixedFitScale = 0;
/** @type {string} */
this._warning = "";
/** @type {boolean} */
this._emulatingMobile = false;
/** @type {?WebInspector.Target} */
this._target = null;
WebInspector.targetManager.observeTargets(this, WebInspector.Target.Type.Page);
}
/** @enum {string} */
WebInspector.DeviceModeModel.Type = {
Mobile: "Mobile",
Tablet: "Tablet",
Desktop: "Desktop",
Device: "Device"
}
WebInspector.DeviceModeModel.MaxDeviceSize = 10000;
/**
* @param {string} value
* @return {string}
*/
WebInspector.DeviceModeModel.deviceSizeValidator = function(value)
{
if (!value || (/^[\d]+$/.test(value) && value >= 0 && value <= WebInspector.OverridesSupport.MaxDeviceSize))
return "";
return WebInspector.UIString("Value must be non-negative integer");
}
WebInspector.DeviceModeModel._touchEventsScriptIdSymbol = Symbol("DeviceModeModel.touchEventsScriptIdSymbol");
// TODO(paulirish): decide on these.
WebInspector.DeviceModeModel._genericMobileUserAgent = "Mozilla/5.0 (Linux; Android 4.4.4; Nexus 5 Build/KTU84P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.114 Mobile Safari/537.36";
WebInspector.DeviceModeModel._genericTabletUserAgent = "Mozilla/5.0 (Linux; Android 4.3; Nexus 7 Build/JSS15Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2307.2 Safari/537.36";
WebInspector.DeviceModeModel.prototype = {
/**
* @param {!Size} size
*/
availableSizeChanged: function(size)
{
this._availableSize = size;
this._calculateAndEmulate(false);
},
/**
* @param {!WebInspector.DeviceModeModel.Type} type
* @param {?WebInspector.EmulatedDevice} device
*/
emulate: function(type, device)
{
this._type = type;
if (type === WebInspector.DeviceModeModel.Type.Device) {
console.assert(device, "Must pass a device for device emulation");
this._device = device;
this._mode = device.modes[0];
} else {
this._device = null;
this._mode = null;
this._genericWidthSetting.removeChangeListener(this._genericWidthSettingChanged, this);
this._genericWidthSetting.set(type === WebInspector.DeviceModeModel.Type.Desktop ? 0 : (type === WebInspector.DeviceModeModel.Type.Mobile ? 480 : 768));
this._genericWidthSetting.addChangeListener(this._genericWidthSettingChanged, this);
}
this._calculateAndEmulate(true);
},
/**
* @return {?WebInspector.EmulatedDevice}
*/
device: function()
{
return this._device;
},
/**
* @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}
*/
fitScale: function()
{
return this._fitScale;
},
/**
* @return {!Size}
*/
appliedDeviceSize: function()
{
return new Size(Math.round(this._screenRect.width / this._fitScale), Math.round(this._screenRect.height / this._fitScale));
},
/**
* @return {!WebInspector.Setting}
*/
fitSetting: function()
{
return this._fitSetting;
},
/**
* @return {!WebInspector.Setting}
*/
genericWidthSetting: function()
{
return this._genericWidthSetting;
},
suspendFitScaleChanges: function()
{
++this._fixedFitScale;
},
resumeFitScaleChanges: function()
{
if (!--this._fixedFitScale)
this._calculateAndEmulate(false);
},
/**
* @return {string}
*/
warning: function()
{
return this._warning;
},
clearWarning: function()
{
this._warning = "";
this._updateCallback.call(this);
},
/**
* @override
* @param {!WebInspector.Target} target
*/
targetAdded: function(target)
{
if (!this._target) {
this._target = target;
this._target.resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.MainFrameNavigated, this.clearWarning, this);
}
},
/**
* @override
* @param {!WebInspector.Target} target
*/
targetRemoved: function(target)
{
if (this._target === target) {
this._target.resourceTreeModel.removeEventListener(WebInspector.ResourceTreeModel.EventTypes.MainFrameNavigated, this.clearWarning, this);
this._target = null;
}
},
_fitSettingChanged: function()
{
this._calculateAndEmulate(false);
},
_genericWidthSettingChanged: 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 = 1;
// Deliberately ignore fixedFitScale.
if (this._fitSetting.get()) {
while (this._availableSize.width < screenWidth * scale || this._availableSize.height < screenHeight * scale)
scale *= 0.8;
}
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 {
// Zero means fill available size.
var screenWidth = this._genericWidthSetting.get() || this._availableSize.width;
var deviceScaleFactor = this._type === WebInspector.DeviceModeModel.Type.Desktop ? 0 : 2;
var mobile = this._type !== WebInspector.DeviceModeModel.Type.Desktop;
var scale = 1;
if (this._fitSetting.get()) {
if (this._fixedFitScale) {
scale = this._fitScale;
} else {
while (this._availableSize.width < screenWidth * scale)
scale *= 0.8;
}
}
var screenHeight = Math.floor(this._availableSize.height / scale);
this._applyDeviceMetrics(new Size(screenWidth, screenHeight), new Insets(0, 0, 0, 0), scale, deviceScaleFactor, mobile, resetScrollAndPageScale);
this._applyUserAgent(
this._type === WebInspector.DeviceModeModel.Type.Mobile ? WebInspector.DeviceModeModel._genericMobileUserAgent :
(this._type === WebInspector.DeviceModeModel.Type.Tablet ? WebInspector.DeviceModeModel._genericTabletUserAgent : ""));
this._applyTouch(this._type !== WebInspector.DeviceModeModel.Type.Desktop, mobile);
if (this._type === WebInspector.DeviceModeModel.Type.Desktop)
this._applyScreenOrientation("");
else
this._applyScreenOrientation(screenHeight >= screenWidth ? "portraitPrimary" : "landscapePrimary");
}
this._updateCallback.call(null);
},
/**
* @param {string} userAgent
*/
_applyUserAgent: function(userAgent)
{
var current = WebInspector.multitargetNetworkManager.userAgentOverride();
if (current !== userAgent) {
WebInspector.multitargetNetworkManager.setUserAgentOverride(userAgent);
if (!this._warning)
this._warning = WebInspector.UIString("You might need to reload the page for proper user agent spoofing and viewport rendering.");
}
},
/**
* @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)
{
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._screenRect = new WebInspector.Rect(
Math.max(0, (this._availableSize.width - screenSize.width * scale) / 2),
Math.max(0, (this._availableSize.height - screenSize.height * scale) / 2),
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._fitScale = scale;
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(apiCallback.bind(this)) :
this._target.emulationAgent().setDeviceMetricsOverride(pageWidth, pageHeight, deviceScaleFactor, mobile, false, scale, 0, 0, screenSize.width, screenSize.height, positionX, positionY, apiCallback.bind(this));
var allPromises = [ setDevicePromise ];
if (resetScrollAndPageScale)
allPromises.push(this._target.emulationAgent().resetScrollAndPageScaleFactor());
return Promise.all(allPromises);
}
/**
* @param {?Protocol.Error} error
* @this {WebInspector.DeviceModeModel}
*/
function apiCallback(error)
{
if (error) {
this._warning = WebInspector.UIString("Screen emulation is not available on this page.");
this._updateCallback.call(null);
this._deviceMetricsOverrideAppliedForTest();
return;
}
if (mobile !== this._emulatingMobile && !this._warning) {
this._warning = WebInspector.UIString("You might need to reload the page for proper user agent spoofing and viewport rendering.");
this._updateCallback.call(null);
}
this._emulatingMobile = mobile;
this._deviceMetricsOverrideAppliedForTest();
}
},
_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));
}
}