Home | History | Annotate | Download | only in front_end
      1 /*
      2  * Copyright (C) 2012 Google Inc. All rights reserved.
      3  *
      4  * Redistribution and use in source and binary forms, with or without
      5  * modification, are permitted provided that the following conditions are
      6  * met:
      7  *
      8  *     * Redistributions of source code must retain the above copyright
      9  * notice, this list of conditions and the following disclaimer.
     10  *     * Redistributions in binary form must reproduce the above
     11  * copyright notice, this list of conditions and the following disclaimer
     12  * in the documentation and/or other materials provided with the
     13  * distribution.
     14  *     * Neither the name of Google Inc. nor the names of its
     15  * contributors may be used to endorse or promote products derived from
     16  * this software without specific prior written permission.
     17  *
     18  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     19  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
     20  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
     21  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
     22  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     23  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
     24  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
     25  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
     26  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     27  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     28  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     29  */
     30 
     31 /**
     32  * @constructor
     33  * @extends {WebInspector.Object}
     34  */
     35 WebInspector.OverridesSupport = function()
     36 {
     37     WebInspector.resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.MainFrameNavigated, this._deviceMetricsChanged.bind(this), this);
     38     this._deviceMetricsOverrideEnabled = false;
     39     this._emulateViewportEnabled = false;
     40 
     41     WebInspector.settings.overrideUserAgent.addChangeListener(this._userAgentChanged, this);
     42     WebInspector.settings.userAgent.addChangeListener(this._userAgentChanged, this);
     43 
     44     WebInspector.settings.overrideDeviceMetrics.addChangeListener(this._deviceMetricsChanged, this);
     45     WebInspector.settings.deviceMetrics.addChangeListener(this._deviceMetricsChanged, this);
     46     WebInspector.settings.emulateViewport.addChangeListener(this._deviceMetricsChanged, this);
     47     WebInspector.settings.deviceFitWindow.addChangeListener(this._deviceMetricsChanged, this);
     48 
     49     WebInspector.settings.overrideGeolocation.addChangeListener(this._geolocationPositionChanged, this);
     50     WebInspector.settings.geolocationOverride.addChangeListener(this._geolocationPositionChanged, this);
     51 
     52     WebInspector.settings.overrideDeviceOrientation.addChangeListener(this._deviceOrientationChanged, this);
     53     WebInspector.settings.deviceOrientationOverride.addChangeListener(this._deviceOrientationChanged, this);
     54 
     55     WebInspector.settings.emulateTouchEvents.addChangeListener(this._emulateTouchEventsChanged, this);
     56 
     57     WebInspector.settings.overrideCSSMedia.addChangeListener(this._cssMediaChanged, this);
     58     WebInspector.settings.emulatedCSSMedia.addChangeListener(this._cssMediaChanged, this);
     59 }
     60 
     61 WebInspector.OverridesSupport.Events = {
     62     OverridesWarningUpdated: "OverridesWarningUpdated",
     63 }
     64 
     65 /**
     66  * @constructor
     67  * @param {number} width
     68  * @param {number} height
     69  * @param {number} deviceScaleFactor
     70  * @param {boolean} textAutosizing
     71  */
     72 WebInspector.OverridesSupport.DeviceMetrics = function(width, height, deviceScaleFactor, textAutosizing)
     73 {
     74     this.width = width;
     75     this.height = height;
     76     this.deviceScaleFactor = deviceScaleFactor;
     77     this.textAutosizing = textAutosizing;
     78 }
     79 
     80 /**
     81  * @return {!WebInspector.OverridesSupport.DeviceMetrics}
     82  */
     83 WebInspector.OverridesSupport.DeviceMetrics.parseSetting = function(value)
     84 {
     85     var width = 0;
     86     var height = 0;
     87     var deviceScaleFactor = 1;
     88     var textAutosizing = true;
     89     if (value) {
     90         var splitMetrics = value.split("x");
     91         if (splitMetrics.length >= 3) {
     92             width = parseInt(splitMetrics[0], 10);
     93             height = parseInt(splitMetrics[1], 10);
     94             deviceScaleFactor = parseFloat(splitMetrics[2]);
     95             if (splitMetrics.length == 4)
     96                 textAutosizing = splitMetrics[3] == 1;
     97         }
     98     }
     99     return new WebInspector.OverridesSupport.DeviceMetrics(width, height, deviceScaleFactor, textAutosizing);
    100 }
    101 
    102 /**
    103  * @return {?WebInspector.OverridesSupport.DeviceMetrics}
    104  */
    105 WebInspector.OverridesSupport.DeviceMetrics.parseUserInput = function(widthString, heightString, deviceScaleFactorString, textAutosizing)
    106 {
    107     function isUserInputValid(value, isInteger)
    108     {
    109         if (!value)
    110             return true;
    111         return isInteger ? /^[0]*[1-9][\d]*$/.test(value) : /^[0]*([1-9][\d]*(\.\d+)?|\.\d+)$/.test(value);
    112     }
    113 
    114     if (!widthString ^ !heightString)
    115         return null;
    116 
    117     var isWidthValid = isUserInputValid(widthString, true);
    118     var isHeightValid = isUserInputValid(heightString, true);
    119     var isDeviceScaleFactorValid = isUserInputValid(deviceScaleFactorString, false);
    120 
    121     if (!isWidthValid && !isHeightValid && !isDeviceScaleFactorValid)
    122         return null;
    123 
    124     var width = isWidthValid ? parseInt(widthString || "0", 10) : -1;
    125     var height = isHeightValid ? parseInt(heightString || "0", 10) : -1;
    126     var deviceScaleFactor = isDeviceScaleFactorValid ? parseFloat(deviceScaleFactorString) : -1;
    127 
    128     return new WebInspector.OverridesSupport.DeviceMetrics(width, height, deviceScaleFactor, textAutosizing);
    129 }
    130 
    131 WebInspector.OverridesSupport.DeviceMetrics.prototype = {
    132     /**
    133      * @return {boolean}
    134      */
    135     isValid: function()
    136     {
    137         return this.isWidthValid() && this.isHeightValid() && this.isDeviceScaleFactorValid();
    138     },
    139 
    140     /**
    141      * @return {boolean}
    142      */
    143     isWidthValid: function()
    144     {
    145         return this.width >= 0;
    146     },
    147 
    148     /**
    149      * @return {boolean}
    150      */
    151     isHeightValid: function()
    152     {
    153         return this.height >= 0;
    154     },
    155 
    156     /**
    157      * @return {boolean}
    158      */
    159     isDeviceScaleFactorValid: function()
    160     {
    161         return this.deviceScaleFactor > 0;
    162     },
    163 
    164     /**
    165      * @return {string}
    166      */
    167     toSetting: function()
    168     {
    169         if (!this.isValid())
    170             return "";
    171 
    172         return this.width && this.height ? this.width + "x" + this.height + "x" + this.deviceScaleFactor + "x" + (this.textAutosizing ? "1" : "0") : "";
    173     },
    174 
    175     /**
    176      * @return {string}
    177      */
    178     widthToInput: function()
    179     {
    180         return this.isWidthValid() && this.width ? String(this.width) : "";
    181     },
    182 
    183     /**
    184      * @return {string}
    185      */
    186     heightToInput: function()
    187     {
    188         return this.isHeightValid() && this.height ? String(this.height) : "";
    189     },
    190 
    191     /**
    192      * @return {string}
    193      */
    194     deviceScaleFactorToInput: function()
    195     {
    196         return this.isDeviceScaleFactorValid() && this.deviceScaleFactor ? String(this.deviceScaleFactor) : "";
    197     },
    198 
    199     /**
    200      * Compute the font scale factor.
    201      *
    202      * Chromium on Android uses a device scale adjustment for fonts used in text autosizing for
    203      * improved legibility. This function computes this adjusted value for text autosizing.
    204      *
    205      * For a description of the Android device scale adjustment algorithm, see:
    206      *     chrome/browser/chrome_content_browser_client.cc, GetFontScaleMultiplier(...)
    207      *
    208      * @return {number} font scale factor.
    209      */
    210     fontScaleFactor: function()
    211     {
    212         if (this.isValid()) {
    213             var minWidth = Math.min(this.width, this.height) / this.deviceScaleFactor;
    214 
    215             var kMinFSM = 1.05;
    216             var kWidthForMinFSM = 320;
    217             var kMaxFSM = 1.3;
    218             var kWidthForMaxFSM = 800;
    219 
    220             if (minWidth <= kWidthForMinFSM)
    221                 return kMinFSM;
    222             if (minWidth >= kWidthForMaxFSM)
    223                 return kMaxFSM;
    224 
    225             // The font scale multiplier varies linearly between kMinFSM and kMaxFSM.
    226             var ratio = (minWidth - kWidthForMinFSM) / (kWidthForMaxFSM - kWidthForMinFSM);
    227 
    228             return ratio * (kMaxFSM - kMinFSM) + kMinFSM;
    229         }
    230 
    231         return 1;
    232     }
    233 }
    234 
    235 /**
    236  * @constructor
    237  * @param {number} latitude
    238  * @param {number} longitude
    239  */
    240 WebInspector.OverridesSupport.GeolocationPosition = function(latitude, longitude, error)
    241 {
    242     this.latitude = latitude;
    243     this.longitude = longitude;
    244     this.error = error;
    245 }
    246 
    247 WebInspector.OverridesSupport.GeolocationPosition.prototype = {
    248     /**
    249      * @return {string}
    250      */
    251     toSetting: function()
    252     {
    253         return (typeof this.latitude === "number" && typeof this.longitude === "number" && typeof this.error === "string") ? this.latitude + "@" + this.longitude + ":" + this.error : "";
    254     }
    255 }
    256 
    257 /**
    258  * @return {!WebInspector.OverridesSupport.GeolocationPosition}
    259  */
    260 WebInspector.OverridesSupport.GeolocationPosition.parseSetting = function(value)
    261 {
    262     if (value) {
    263         var splitError = value.split(":");
    264         if (splitError.length === 2) {
    265             var splitPosition = splitError[0].split("@")
    266             if (splitPosition.length === 2)
    267                 return new WebInspector.OverridesSupport.GeolocationPosition(parseFloat(splitPosition[0]), parseFloat(splitPosition[1]), splitError[1]);
    268         }
    269     }
    270     return new WebInspector.OverridesSupport.GeolocationPosition(0, 0, "");
    271 }
    272 
    273 /**
    274  * @return {?WebInspector.OverridesSupport.GeolocationPosition}
    275  */
    276 WebInspector.OverridesSupport.GeolocationPosition.parseUserInput = function(latitudeString, longitudeString, errorStatus)
    277 {
    278     function isUserInputValid(value)
    279     {
    280         if (!value)
    281             return true;
    282         return /^[-]?[0-9]*[.]?[0-9]*$/.test(value);
    283     }
    284 
    285     if (!latitudeString ^ !latitudeString)
    286         return null;
    287 
    288     var isLatitudeValid = isUserInputValid(latitudeString);
    289     var isLongitudeValid = isUserInputValid(longitudeString);
    290 
    291     if (!isLatitudeValid && !isLongitudeValid)
    292         return null;
    293 
    294     var latitude = isLatitudeValid ? parseFloat(latitudeString) : -1;
    295     var longitude = isLongitudeValid ? parseFloat(longitudeString) : -1;
    296 
    297     return new WebInspector.OverridesSupport.GeolocationPosition(latitude, longitude, errorStatus ? "PositionUnavailable" : "");
    298 }
    299 
    300 WebInspector.OverridesSupport.GeolocationPosition.clearGeolocationOverride = function()
    301 {
    302     PageAgent.clearGeolocationOverride();
    303 }
    304 
    305 /**
    306  * @constructor
    307  * @param {number} alpha
    308  * @param {number} beta
    309  * @param {number} gamma
    310  */
    311 WebInspector.OverridesSupport.DeviceOrientation = function(alpha, beta, gamma)
    312 {
    313     this.alpha = alpha;
    314     this.beta = beta;
    315     this.gamma = gamma;
    316 }
    317 
    318 WebInspector.OverridesSupport.DeviceOrientation.prototype = {
    319     /**
    320      * @return {string}
    321      */
    322     toSetting: function()
    323     {
    324         return JSON.stringify(this);
    325     }
    326 }
    327 
    328 /**
    329  * @return {!WebInspector.OverridesSupport.DeviceOrientation}
    330  */
    331 WebInspector.OverridesSupport.DeviceOrientation.parseSetting = function(value)
    332 {
    333     if (value) {
    334         var jsonObject = JSON.parse(value);
    335         return new WebInspector.OverridesSupport.DeviceOrientation(jsonObject.alpha, jsonObject.beta, jsonObject.gamma);
    336     }
    337     return new WebInspector.OverridesSupport.DeviceOrientation(0, 0, 0);
    338 }
    339 
    340 /**
    341  * @return {?WebInspector.OverridesSupport.DeviceOrientation}
    342  */
    343 WebInspector.OverridesSupport.DeviceOrientation.parseUserInput = function(alphaString, betaString, gammaString)
    344 {
    345     function isUserInputValid(value)
    346     {
    347         if (!value)
    348             return true;
    349         return /^[-]?[0-9]*[.]?[0-9]*$/.test(value);
    350     }
    351 
    352     if (!alphaString ^ !betaString ^ !gammaString)
    353         return null;
    354 
    355     var isAlphaValid = isUserInputValid(alphaString);
    356     var isBetaValid = isUserInputValid(betaString);
    357     var isGammaValid = isUserInputValid(gammaString);
    358 
    359     if (!isAlphaValid && !isBetaValid && !isGammaValid)
    360         return null;
    361 
    362     var alpha = isAlphaValid ? parseFloat(alphaString) : -1;
    363     var beta = isBetaValid ? parseFloat(betaString) : -1;
    364     var gamma = isGammaValid ? parseFloat(gammaString) : -1;
    365 
    366     return new WebInspector.OverridesSupport.DeviceOrientation(alpha, beta, gamma);
    367 }
    368 
    369 WebInspector.OverridesSupport.DeviceOrientation.clearDeviceOrientationOverride = function()
    370 {
    371     PageAgent.clearDeviceOrientationOverride();
    372 }
    373 
    374 WebInspector.OverridesSupport.prototype = {
    375     /**
    376      * @param {string} deviceMetrics
    377      * @param {string} userAgent
    378      */
    379     emulateDevice: function(deviceMetrics, userAgent)
    380     {
    381         this._deviceMetricsChangedListenerMuted = true;
    382         WebInspector.settings.deviceMetrics.set(deviceMetrics);
    383         WebInspector.settings.userAgent.set(userAgent);
    384         WebInspector.settings.overrideDeviceMetrics.set(true);
    385         WebInspector.settings.overrideUserAgent.set(true);
    386         WebInspector.settings.emulateTouchEvents.set(true);
    387         WebInspector.settings.emulateViewport.set(true);
    388         delete this._deviceMetricsChangedListenerMuted;
    389         this._deviceMetricsChanged();
    390     },
    391 
    392     reset: function()
    393     {
    394         this._deviceMetricsChangedListenerMuted = true;
    395         WebInspector.settings.overrideDeviceMetrics.set(false);
    396         WebInspector.settings.overrideUserAgent.set(false);
    397         WebInspector.settings.emulateTouchEvents.set(false);
    398         WebInspector.settings.overrideDeviceOrientation.set(false);
    399         WebInspector.settings.overrideGeolocation.set(false);
    400         WebInspector.settings.overrideCSSMedia.set(false);
    401         WebInspector.settings.emulateViewport.set(false);
    402         WebInspector.settings.deviceMetrics.set("");
    403         delete this._deviceMetricsChangedListenerMuted;
    404         this._deviceMetricsChanged();
    405     },
    406 
    407     applyInitialOverrides: function()
    408     {
    409         this._deviceMetricsChangedListenerMuted = true;
    410         this._userAgentChanged();
    411         this._deviceMetricsChanged();
    412         this._deviceOrientationChanged();
    413         this._geolocationPositionChanged();
    414         this._emulateTouchEventsChanged();
    415         this._cssMediaChanged();
    416         delete this._deviceMetricsChangedListenerMuted;
    417         this._deviceMetricsChanged();
    418         this._revealOverridesTabIfNeeded();
    419     },
    420 
    421     _userAgentChanged: function()
    422     {
    423         if (WebInspector.isInspectingDevice())
    424             return;
    425         NetworkAgent.setUserAgentOverride(WebInspector.settings.overrideUserAgent.get() ? WebInspector.settings.userAgent.get() : "");
    426     },
    427 
    428     _deviceMetricsChanged: function()
    429     {
    430         if (this._deviceMetricsChangedListenerMuted)
    431             return;
    432         var metrics = WebInspector.OverridesSupport.DeviceMetrics.parseSetting(WebInspector.settings.overrideDeviceMetrics.get() ? WebInspector.settings.deviceMetrics.get() : "");
    433         if (!metrics.isValid())
    434             return;
    435 
    436         var dipWidth = Math.round(metrics.width / metrics.deviceScaleFactor);
    437         var dipHeight = Math.round(metrics.height / metrics.deviceScaleFactor);
    438 
    439         // Disable override without checks.
    440         if (dipWidth && dipHeight && WebInspector.isInspectingDevice()) {
    441             this._updateWarningMessage(WebInspector.UIString("Screen emulation on the device is not available."));
    442             return;
    443         }
    444 
    445         PageAgent.setDeviceMetricsOverride(dipWidth, dipHeight, metrics.deviceScaleFactor, WebInspector.settings.emulateViewport.get(), WebInspector.settings.deviceFitWindow.get(), metrics.textAutosizing, metrics.fontScaleFactor(), apiCallback.bind(this));
    446 
    447         /**
    448          * @param {?Protocol.Error} error
    449          * @this {WebInspector.OverridesSupport}
    450          */
    451         function apiCallback(error)
    452         {
    453             if (error) {
    454                 this._updateWarningMessage(WebInspector.UIString("Screen emulation is not available on this page."));
    455                 return;
    456             }
    457 
    458             var metricsOverrideEnabled = !!(dipWidth && dipHeight);
    459             var viewportEnabled =  WebInspector.settings.emulateViewport.get();
    460             this._updateWarningMessage(this._deviceMetricsOverrideEnabled !== metricsOverrideEnabled || (metricsOverrideEnabled && this._emulateViewportEnabled != viewportEnabled) ?
    461                 WebInspector.UIString("You might need to reload the page for proper user agent spoofing and viewport rendering.") : "");
    462             this._deviceMetricsOverrideEnabled = metricsOverrideEnabled;
    463             this._emulateViewportEnabled = viewportEnabled;
    464         }
    465     },
    466 
    467     _geolocationPositionChanged: function()
    468     {
    469         if (!WebInspector.settings.overrideGeolocation.get()) {
    470             PageAgent.clearGeolocationOverride();
    471             return;
    472         }
    473         var geolocation = WebInspector.OverridesSupport.GeolocationPosition.parseSetting(WebInspector.settings.geolocationOverride.get());
    474         if (geolocation.error)
    475             PageAgent.setGeolocationOverride();
    476         else
    477             PageAgent.setGeolocationOverride(geolocation.latitude, geolocation.longitude, 150);
    478     },
    479 
    480     _deviceOrientationChanged: function()
    481     {
    482         if (!WebInspector.settings.overrideDeviceOrientation.get()) {
    483             PageAgent.clearDeviceOrientationOverride();
    484             return;
    485         }
    486         if (WebInspector.isInspectingDevice())
    487             return;
    488 
    489         var deviceOrientation = WebInspector.OverridesSupport.DeviceOrientation.parseSetting(WebInspector.settings.deviceOrientationOverride.get());
    490         PageAgent.setDeviceOrientationOverride(deviceOrientation.alpha, deviceOrientation.beta, deviceOrientation.gamma);
    491     },
    492 
    493     _emulateTouchEventsChanged: function()
    494     {
    495         if (WebInspector.isInspectingDevice() && WebInspector.settings.emulateTouchEvents.get())
    496             return;
    497 
    498         WebInspector.domAgent.emulateTouchEventObjects(WebInspector.settings.emulateTouchEvents.get());
    499     },
    500 
    501     _cssMediaChanged: function()
    502     {
    503         PageAgent.setEmulatedMedia(WebInspector.settings.overrideCSSMedia.get() ? WebInspector.settings.emulatedCSSMedia.get() : "");
    504         WebInspector.cssModel.mediaQueryResultChanged();
    505     },
    506 
    507     _anyOverrideIsEnabled: function()
    508     {
    509         return WebInspector.settings.overrideUserAgent.get() || WebInspector.settings.overrideDeviceMetrics.get() ||
    510             WebInspector.settings.overrideGeolocation.get() || WebInspector.settings.overrideDeviceOrientation.get() ||
    511             WebInspector.settings.emulateTouchEvents.get() || WebInspector.settings.overrideCSSMedia.get();
    512     },
    513 
    514     _revealOverridesTabIfNeeded: function()
    515     {
    516         if (this._anyOverrideIsEnabled()) {
    517             if (!WebInspector.settings.showEmulationViewInDrawer.get())
    518                 WebInspector.settings.showEmulationViewInDrawer.set(true);
    519             WebInspector.inspectorView.showViewInDrawer("emulation");
    520         }
    521     },
    522 
    523     /**
    524      * @param {string} warningMessage
    525      */
    526     _updateWarningMessage: function(warningMessage)
    527     {
    528         this._warningMessage = warningMessage;
    529         this.dispatchEventToListeners(WebInspector.OverridesSupport.Events.OverridesWarningUpdated);
    530     },
    531 
    532     /**
    533      * @return {string}
    534      */
    535     warningMessage: function()
    536     {
    537         return this._warningMessage || "";
    538     },
    539 
    540     __proto__: WebInspector.Object.prototype
    541 }
    542 
    543 
    544 /**
    545  * @type {!WebInspector.OverridesSupport}
    546  */
    547 WebInspector.overridesSupport;
    548