Home | History | Annotate | Download | only in front_end
      1 /*
      2  * Copyright (C) 2013 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.VBox}
     34  * @implements {WebInspector.DOMNodeHighlighter}
     35  * @param {!WebInspector.Target} target
     36  */
     37 WebInspector.ScreencastView = function(target)
     38 {
     39     WebInspector.VBox.call(this);
     40     this._target = target;
     41 
     42     this.setMinimumSize(150, 150);
     43     this.registerRequiredCSS("screencastView.css");
     44 };
     45 
     46 WebInspector.ScreencastView._bordersSize = 44;
     47 
     48 WebInspector.ScreencastView._navBarHeight = 29;
     49 
     50 WebInspector.ScreencastView._HttpRegex = /^https?:\/\/(.+)/;
     51 
     52 WebInspector.ScreencastView.prototype = {
     53     initialize: function()
     54     {
     55         this.element.classList.add("screencast");
     56 
     57         this._createNavigationBar();
     58 
     59         this._viewportElement = this.element.createChild("div", "screencast-viewport hidden");
     60         this._canvasContainerElement = this._viewportElement.createChild("div", "screencast-canvas-container");
     61         this._glassPaneElement = this._canvasContainerElement.createChild("div", "screencast-glasspane hidden");
     62 
     63         this._canvasElement = this._canvasContainerElement.createChild("canvas");
     64         this._canvasElement.tabIndex = 1;
     65         this._canvasElement.addEventListener("mousedown", this._handleMouseEvent.bind(this), false);
     66         this._canvasElement.addEventListener("mouseup", this._handleMouseEvent.bind(this), false);
     67         this._canvasElement.addEventListener("mousemove", this._handleMouseEvent.bind(this), false);
     68         this._canvasElement.addEventListener("mousewheel", this._handleMouseEvent.bind(this), false);
     69         this._canvasElement.addEventListener("click", this._handleMouseEvent.bind(this), false);
     70         this._canvasElement.addEventListener("contextmenu", this._handleContextMenuEvent.bind(this), false);
     71         this._canvasElement.addEventListener("keydown", this._handleKeyEvent.bind(this), false);
     72         this._canvasElement.addEventListener("keyup", this._handleKeyEvent.bind(this), false);
     73         this._canvasElement.addEventListener("keypress", this._handleKeyEvent.bind(this), false);
     74 
     75         this._titleElement = this._canvasContainerElement.createChild("div", "screencast-element-title monospace hidden");
     76         this._tagNameElement = this._titleElement.createChild("span", "screencast-tag-name");
     77         this._nodeIdElement = this._titleElement.createChild("span", "screencast-node-id");
     78         this._classNameElement = this._titleElement.createChild("span", "screencast-class-name");
     79         this._titleElement.appendChild(document.createTextNode(" "));
     80         this._nodeWidthElement = this._titleElement.createChild("span");
     81         this._titleElement.createChild("span", "screencast-px").textContent = "px";
     82         this._titleElement.appendChild(document.createTextNode(" \u00D7 "));
     83         this._nodeHeightElement = this._titleElement.createChild("span");
     84         this._titleElement.createChild("span", "screencast-px").textContent = "px";
     85 
     86         this._imageElement = new Image();
     87         this._isCasting = false;
     88         this._context = this._canvasElement.getContext("2d");
     89         this._checkerboardPattern = this._createCheckerboardPattern(this._context);
     90 
     91         this._shortcuts = /** !Object.<number, function(Event=):boolean> */ ({});
     92         this._shortcuts[WebInspector.KeyboardShortcut.makeKey("l", WebInspector.KeyboardShortcut.Modifiers.Ctrl)] = this._focusNavigationBar.bind(this);
     93 
     94         WebInspector.resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.ScreencastFrame, this._screencastFrame, this);
     95         WebInspector.resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.ScreencastVisibilityChanged, this._screencastVisibilityChanged, this);
     96 
     97         WebInspector.timelineManager.addEventListener(WebInspector.TimelineManager.EventTypes.TimelineStarted, this._onTimeline.bind(this, true), this);
     98         WebInspector.timelineManager.addEventListener(WebInspector.TimelineManager.EventTypes.TimelineStopped, this._onTimeline.bind(this, false), this);
     99         this._timelineActive = WebInspector.timelineManager.isStarted();
    100 
    101         WebInspector.cpuProfilerModel.addEventListener(WebInspector.CPUProfilerModel.EventTypes.ProfileStarted, this._onProfiler.bind(this, true), this);
    102         WebInspector.cpuProfilerModel.addEventListener(WebInspector.CPUProfilerModel.EventTypes.ProfileStopped, this._onProfiler.bind(this, false), this);
    103         this._profilerActive = WebInspector.cpuProfilerModel.isRecordingProfile();
    104 
    105         this._updateGlasspane();
    106     },
    107 
    108     wasShown: function()
    109     {
    110         this._startCasting();
    111     },
    112 
    113     willHide: function()
    114     {
    115         this._stopCasting();
    116     },
    117 
    118     _startCasting: function()
    119     {
    120         if (this._timelineActive || this._profilerActive)
    121             return;
    122         if (this._isCasting)
    123             return;
    124         this._isCasting = true;
    125 
    126         const maxImageDimension = 1024;
    127         var dimensions = this._viewportDimensions();
    128         if (dimensions.width < 0 || dimensions.height < 0) {
    129             this._isCasting = false;
    130             return;
    131         }
    132         dimensions.width *= WebInspector.zoomManager.zoomFactor();
    133         dimensions.height *= WebInspector.zoomManager.zoomFactor();
    134         this._target.pageAgent().startScreencast("jpeg", 80, Math.min(maxImageDimension, dimensions.width), Math.min(maxImageDimension, dimensions.height));
    135         this._target.domModel.setHighlighter(this);
    136     },
    137 
    138     _stopCasting: function()
    139     {
    140         if (!this._isCasting)
    141             return;
    142         this._isCasting = false;
    143         this._target.pageAgent().stopScreencast();
    144         this._target.domModel.setHighlighter(null);
    145     },
    146 
    147     /**
    148      * @param {!WebInspector.Event} event
    149      */
    150     _screencastFrame: function(event)
    151     {
    152         var metadata = /** type {PageAgent.ScreencastFrameMetadata} */(event.data.metadata);
    153 
    154         if (!metadata.deviceScaleFactor) {
    155           console.log(event.data.data);
    156           return;
    157         }
    158 
    159         var base64Data = /** type {string} */(event.data.data);
    160         this._imageElement.src = "data:image/jpg;base64," + base64Data;
    161         this._deviceScaleFactor = metadata.deviceScaleFactor;
    162         this._pageScaleFactor = metadata.pageScaleFactor;
    163         this._viewport = metadata.viewport;
    164         if (!this._viewport)
    165             return;
    166         var offsetTop = metadata.offsetTop || 0;
    167         var offsetBottom = metadata.offsetBottom || 0;
    168 
    169         var screenWidthDIP = this._viewport.width * this._pageScaleFactor;
    170         var screenHeightDIP = this._viewport.height * this._pageScaleFactor + offsetTop + offsetBottom;
    171         this._screenOffsetTop = offsetTop;
    172         this._resizeViewport(screenWidthDIP, screenHeightDIP);
    173 
    174         this._imageZoom = this._imageElement.naturalWidth ? this._canvasElement.offsetWidth / this._imageElement.naturalWidth : 1;
    175         this.highlightDOMNode(this._highlightNode, this._highlightConfig);
    176     },
    177 
    178     _isGlassPaneActive: function()
    179     {
    180         return !this._glassPaneElement.classList.contains("hidden");
    181     },
    182 
    183     /**
    184      * @param {!WebInspector.Event} event
    185      */
    186     _screencastVisibilityChanged: function(event)
    187     {
    188         this._targetInactive = !event.data.visible;
    189         this._updateGlasspane();
    190     },
    191 
    192     /**
    193      * @param {boolean} on
    194      * @private
    195      */
    196     _onTimeline: function(on)
    197     {
    198         this._timelineActive = on;
    199         if (this._timelineActive)
    200             this._stopCasting();
    201         else
    202             this._startCasting();
    203         this._updateGlasspane();
    204     },
    205 
    206     /**
    207      * @param {boolean} on
    208      * @param {!WebInspector.Event} event
    209      * @private
    210      */
    211     _onProfiler: function(on, event) {
    212         this._profilerActive = on;
    213         if (this._profilerActive)
    214             this._stopCasting();
    215         else
    216             this._startCasting();
    217         this._updateGlasspane();
    218     },
    219 
    220     _updateGlasspane: function()
    221     {
    222         if (this._targetInactive) {
    223             this._glassPaneElement.textContent = WebInspector.UIString("The tab is inactive");
    224             this._glassPaneElement.classList.remove("hidden");
    225         } else if (this._timelineActive) {
    226             this._glassPaneElement.textContent = WebInspector.UIString("Timeline is active");
    227             this._glassPaneElement.classList.remove("hidden");
    228         } else if (this._profilerActive) {
    229             this._glassPaneElement.textContent = WebInspector.UIString("CPU profiler is active");
    230             this._glassPaneElement.classList.remove("hidden");
    231         } else {
    232             this._glassPaneElement.classList.add("hidden");
    233         }
    234     },
    235 
    236     /**
    237      * @param {number} screenWidthDIP
    238      * @param {number} screenHeightDIP
    239      */
    240     _resizeViewport: function(screenWidthDIP, screenHeightDIP)
    241     {
    242         var dimensions = this._viewportDimensions();
    243         this._screenZoom = Math.min(dimensions.width / screenWidthDIP, dimensions.height / screenHeightDIP);
    244 
    245         var bordersSize = WebInspector.ScreencastView._bordersSize;
    246         this._viewportElement.classList.remove("hidden");
    247         this._viewportElement.style.width = screenWidthDIP * this._screenZoom + bordersSize + "px";
    248         this._viewportElement.style.height = screenHeightDIP * this._screenZoom + bordersSize + "px";
    249     },
    250 
    251     /**
    252      * @param {?Event} event
    253      */
    254     _handleMouseEvent: function(event)
    255     {
    256         if (this._isGlassPaneActive()) {
    257           event.consume();
    258           return;
    259         }
    260 
    261         if (!this._viewport)
    262             return;
    263 
    264         if (!this._inspectModeConfig || event.type === "mousewheel") {
    265             this._simulateTouchGestureForMouseEvent(event);
    266             event.preventDefault();
    267             if (event.type === "mousedown")
    268                 this._canvasElement.focus();
    269             return;
    270         }
    271 
    272         var position = this._convertIntoScreenSpace(event);
    273         this._target.domModel.nodeForLocation(position.x / this._pageScaleFactor, position.y / this._pageScaleFactor, callback.bind(this));
    274 
    275         /**
    276          * @param {?WebInspector.DOMNode} node
    277          * @this {WebInspector.ScreencastView}
    278          */
    279         function callback(node)
    280         {
    281             if (!node)
    282                 return;
    283             if (event.type === "mousemove")
    284                 this.highlightDOMNode(node, this._inspectModeConfig);
    285             else if (event.type === "click")
    286                 node.reveal();
    287         }
    288     },
    289 
    290     /**
    291      * @param {?Event} event
    292      */
    293     _handleKeyEvent: function(event)
    294     {
    295         if (this._isGlassPaneActive()) {
    296             event.consume();
    297             return;
    298         }
    299 
    300         var shortcutKey = WebInspector.KeyboardShortcut.makeKeyFromEvent(/** @type {!KeyboardEvent} */ (event));
    301         var handler = this._shortcuts[shortcutKey];
    302         if (handler && handler(event)) {
    303             event.consume();
    304             return;
    305         }
    306 
    307         var type;
    308         switch (event.type) {
    309         case "keydown": type = "keyDown"; break;
    310         case "keyup": type = "keyUp"; break;
    311         case "keypress": type = "char"; break;
    312         default: return;
    313         }
    314 
    315         var text = event.type === "keypress" ? String.fromCharCode(event.charCode) : undefined;
    316         InputAgent.dispatchKeyEvent(type, this._modifiersForEvent(event), event.timeStamp / 1000, text, text ? text.toLowerCase() : undefined,
    317                                     event.keyIdentifier, event.keyCode /* windowsVirtualKeyCode */, event.keyCode /* nativeVirtualKeyCode */, false, false, false);
    318         event.consume();
    319         this._canvasElement.focus();
    320     },
    321 
    322     /**
    323      * @param {?Event} event
    324      */
    325     _handleContextMenuEvent: function(event)
    326     {
    327         event.consume(true);
    328     },
    329 
    330     /**
    331      * @param {?Event} event
    332      */
    333     _simulateTouchGestureForMouseEvent: function(event)
    334     {
    335         var convertedPosition = this._convertIntoScreenSpace(event);
    336         var zoomedPosition = this._zoomIntoScreenSpace(event);
    337         var timeStamp = event.timeStamp / 1000;
    338 
    339         /**
    340          * @this {!WebInspector.ScreencastView}
    341          */
    342         function clearPinch()
    343         {
    344             delete this._lastPinchAnchor;
    345             delete this._lastPinchZoomedY;
    346             delete this._lastPinchScale;
    347         }
    348 
    349         /**
    350          * @this {!WebInspector.ScreencastView}
    351          */
    352         function clearScroll()
    353         {
    354             delete this._lastScrollZoomedPosition;
    355         }
    356 
    357         /**
    358          * @this {!WebInspector.ScreencastView}
    359          */
    360         function scrollBegin()
    361         {
    362             InputAgent.dispatchGestureEvent("scrollBegin", convertedPosition.x, convertedPosition.y, timeStamp);
    363             this._lastScrollZoomedPosition = zoomedPosition;
    364             clearPinch.call(this);
    365         }
    366 
    367         /**
    368          * @this {!WebInspector.ScreencastView}
    369          */
    370         function scrollUpdate()
    371         {
    372             var dx = this._lastScrollZoomedPosition ? zoomedPosition.x - this._lastScrollZoomedPosition.x : 0;
    373             var dy = this._lastScrollZoomedPosition ? zoomedPosition.y - this._lastScrollZoomedPosition.y : 0;
    374             if (dx || dy) {
    375                 InputAgent.dispatchGestureEvent("scrollUpdate", convertedPosition.x, convertedPosition.y, timeStamp, dx, dy);
    376                 this._lastScrollZoomedPosition = zoomedPosition;
    377             }
    378         }
    379 
    380         /**
    381          * @this {!WebInspector.ScreencastView}
    382          */
    383         function scrollEnd()
    384         {
    385             if (this._lastScrollZoomedPosition) {
    386                 InputAgent.dispatchGestureEvent("scrollEnd", convertedPosition.x, convertedPosition.y, timeStamp);
    387                 clearScroll.call(this);
    388                 return true;
    389             }
    390             return false;
    391         }
    392 
    393         /**
    394          * @this {!WebInspector.ScreencastView}
    395          */
    396         function pinchBegin()
    397         {
    398             InputAgent.dispatchGestureEvent("pinchBegin", convertedPosition.x, convertedPosition.y, timeStamp);
    399             this._lastPinchAnchor = convertedPosition;
    400             this._lastPinchZoomedY = zoomedPosition.y;
    401             this._lastPinchScale = 1;
    402             clearScroll.call(this);
    403         }
    404 
    405         /**
    406          * @this {!WebInspector.ScreencastView}
    407          */
    408         function pinchUpdate()
    409         {
    410             var dy = this._lastPinchZoomedY ? this._lastPinchZoomedY - zoomedPosition.y : 0;
    411             if (dy) {
    412                 var scale = Math.exp(dy * 0.002);
    413                 InputAgent.dispatchGestureEvent("pinchUpdate", this._lastPinchAnchor.x, this._lastPinchAnchor.y, timeStamp, 0, 0, scale / this._lastPinchScale);
    414                 this._lastPinchScale = scale;
    415             }
    416         }
    417 
    418         /**
    419          * @this {!WebInspector.ScreencastView}
    420          */
    421         function pinchEnd()
    422         {
    423             if (this._lastPinchAnchor) {
    424                 InputAgent.dispatchGestureEvent("pinchEnd", this._lastPinchAnchor.x, this._lastPinchAnchor.y, timeStamp);
    425                 clearPinch.call(this);
    426                 return true;
    427             }
    428             return false;
    429         }
    430 
    431         switch (event.which) {
    432         case 1: // Left
    433             if (event.type === "mousedown") {
    434                 if (event.shiftKey) {
    435                     pinchBegin.call(this);
    436                 } else {
    437                     scrollBegin.call(this);
    438                 }
    439             } else if (event.type === "mousemove") {
    440                 if (event.shiftKey) {
    441                     if (scrollEnd.call(this))
    442                         pinchBegin.call(this);
    443                     pinchUpdate.call(this);
    444                 } else {
    445                     if (pinchEnd.call(this))
    446                         scrollBegin.call(this);
    447                     scrollUpdate.call(this);
    448                 }
    449             } else if (event.type === "mouseup") {
    450                 pinchEnd.call(this);
    451                 scrollEnd.call(this);
    452             } else if (event.type === "mousewheel") {
    453                 if (!this._lastPinchAnchor && !this._lastScrollZoomedPosition) {
    454                     if (event.shiftKey) {
    455                         var factor = 1.1;
    456                         var scale = event.wheelDeltaY < 0 ? 1 / factor : factor;
    457                         InputAgent.dispatchGestureEvent("pinchBegin", convertedPosition.x, convertedPosition.y, timeStamp);
    458                         InputAgent.dispatchGestureEvent("pinchUpdate", convertedPosition.x, convertedPosition.y, timeStamp, 0, 0, scale);
    459                         InputAgent.dispatchGestureEvent("pinchEnd", convertedPosition.x, convertedPosition.y, timeStamp);
    460                     } else {
    461                         InputAgent.dispatchGestureEvent("scrollBegin", convertedPosition.x, convertedPosition.y, timeStamp);
    462                         InputAgent.dispatchGestureEvent("scrollUpdate", convertedPosition.x, convertedPosition.y, timeStamp, event.wheelDeltaX, event.wheelDeltaY);
    463                         InputAgent.dispatchGestureEvent("scrollEnd", convertedPosition.x, convertedPosition.y, timeStamp);
    464                     }
    465                 }
    466             } else if (event.type === "click") {
    467                 if (!event.shiftKey) {
    468                     InputAgent.dispatchMouseEvent("mousePressed", convertedPosition.x, convertedPosition.y, 0, timeStamp, "left", 1, true);
    469                     InputAgent.dispatchMouseEvent("mouseReleased", convertedPosition.x, convertedPosition.y, 0, timeStamp, "left", 1, true);
    470                     // FIXME: migrate to tap once it dispatches clicks again.
    471                     // InputAgent.dispatchGestureEvent("tapDown", x, y, timeStamp);
    472                     // InputAgent.dispatchGestureEvent("tap", x, y, timeStamp);
    473                 }
    474             }
    475             break;
    476 
    477         case 2: // Middle
    478             if (event.type === "mousedown") {
    479                 InputAgent.dispatchGestureEvent("tapDown", convertedPosition.x, convertedPosition.y, timeStamp);
    480             } else if (event.type === "mouseup") {
    481                 InputAgent.dispatchGestureEvent("tap", convertedPosition.x, convertedPosition.y, timeStamp);
    482             }
    483             break;
    484 
    485         case 3: // Right
    486         case 0: // None
    487         default:
    488         }
    489     },
    490 
    491     /**
    492      * @param {?Event} event
    493      * @return {!{x: number, y: number}}
    494      */
    495     _zoomIntoScreenSpace: function(event)
    496     {
    497         var zoom = this._canvasElement.offsetWidth / this._viewport.width / this._pageScaleFactor;
    498         var position  = {};
    499         position.x = Math.round(event.offsetX / zoom);
    500         position.y = Math.round(event.offsetY / zoom);
    501         return position;
    502     },
    503 
    504     /**
    505      * @param {?Event} event
    506      * @return {!{x: number, y: number}}
    507      */
    508     _convertIntoScreenSpace: function(event)
    509     {
    510         var position = this._zoomIntoScreenSpace(event);
    511         position.y = Math.round(position.y - this._screenOffsetTop);
    512         return position;
    513     },
    514 
    515     /**
    516      * @param {?Event} event
    517      * @return {number}
    518      */
    519     _modifiersForEvent: function(event)
    520     {
    521         var modifiers = 0;
    522         if (event.altKey)
    523             modifiers = 1;
    524         if (event.ctrlKey)
    525             modifiers += 2;
    526         if (event.metaKey)
    527             modifiers += 4;
    528         if (event.shiftKey)
    529             modifiers += 8;
    530         return modifiers;
    531     },
    532 
    533     onResize: function()
    534     {
    535         if (this._deferredCasting) {
    536             clearTimeout(this._deferredCasting);
    537             delete this._deferredCasting;
    538         }
    539 
    540         this._stopCasting();
    541         this._deferredCasting = setTimeout(this._startCasting.bind(this), 100);
    542     },
    543 
    544     /**
    545      * @param {?WebInspector.DOMNode} node
    546      * @param {?DOMAgent.HighlightConfig} config
    547      * @param {!RuntimeAgent.RemoteObjectId=} objectId
    548      */
    549     highlightDOMNode: function(node, config, objectId)
    550     {
    551         this._highlightNode = node;
    552         this._highlightConfig = config;
    553         if (!node) {
    554             this._model = null;
    555             this._config = null;
    556             this._node = null;
    557             this._titleElement.classList.add("hidden");
    558             this._repaint();
    559             return;
    560         }
    561 
    562         this._node = node;
    563         node.boxModel(callback.bind(this));
    564 
    565         /**
    566          * @param {?DOMAgent.BoxModel} model
    567          * @this {WebInspector.ScreencastView}
    568          */
    569         function callback(model)
    570         {
    571             if (!model) {
    572                 this._repaint();
    573                 return;
    574             }
    575             this._model = this._scaleModel(model);
    576             this._config = config;
    577             this._repaint();
    578         }
    579     },
    580 
    581     /**
    582      * @param {!DOMAgent.BoxModel} model
    583      * @return {!DOMAgent.BoxModel}
    584      */
    585     _scaleModel: function(model)
    586     {
    587         var scale = this._canvasElement.offsetWidth / this._viewport.width;
    588 
    589         /**
    590          * @param {!DOMAgent.Quad} quad
    591          * @this {WebInspector.ScreencastView}
    592          */
    593         function scaleQuad(quad)
    594         {
    595             for (var i = 0; i < quad.length; i += 2) {
    596                 quad[i] = (quad[i] - this._viewport.x) * scale;
    597                 quad[i + 1] = (quad[i + 1] - this._viewport.y) * scale + this._screenOffsetTop * this._screenZoom;
    598             }
    599         }
    600 
    601         scaleQuad.call(this, model.content);
    602         scaleQuad.call(this, model.padding);
    603         scaleQuad.call(this, model.border);
    604         scaleQuad.call(this, model.margin);
    605         return model;
    606     },
    607 
    608     _repaint: function()
    609     {
    610         var model = this._model;
    611         var config = this._config;
    612 
    613         this._canvasElement.width = window.devicePixelRatio * this._canvasElement.offsetWidth;
    614         this._canvasElement.height = window.devicePixelRatio * this._canvasElement.offsetHeight;
    615 
    616         this._context.save();
    617         this._context.scale(window.devicePixelRatio, window.devicePixelRatio);
    618 
    619         // Paint top and bottom gutter.
    620         this._context.save();
    621         this._context.fillStyle = this._checkerboardPattern;
    622         this._context.fillRect(0, 0, this._canvasElement.offsetWidth, this._screenOffsetTop * this._screenZoom);
    623         this._context.fillRect(0, this._screenOffsetTop * this._screenZoom + this._imageElement.naturalHeight * this._imageZoom, this._canvasElement.offsetWidth, this._canvasElement.offsetHeight);
    624         this._context.restore();
    625 
    626         if (model && config) {
    627             this._context.save();
    628             const transparentColor = "rgba(0, 0, 0, 0)";
    629             var hasContent = model.content && config.contentColor !== transparentColor;
    630             var hasPadding = model.padding && config.paddingColor !== transparentColor;
    631             var hasBorder = model.border && config.borderColor !== transparentColor;
    632             var hasMargin = model.margin && config.marginColor !== transparentColor;
    633 
    634             var clipQuad;
    635             if (hasMargin && (!hasBorder || !this._quadsAreEqual(model.margin, model.border))) {
    636                 this._drawOutlinedQuadWithClip(model.margin, model.border, config.marginColor);
    637                 clipQuad = model.border;
    638             }
    639             if (hasBorder && (!hasPadding || !this._quadsAreEqual(model.border, model.padding))) {
    640                 this._drawOutlinedQuadWithClip(model.border, model.padding, config.borderColor);
    641                 clipQuad = model.padding;
    642             }
    643             if (hasPadding && (!hasContent || !this._quadsAreEqual(model.padding, model.content))) {
    644                 this._drawOutlinedQuadWithClip(model.padding, model.content, config.paddingColor);
    645                 clipQuad = model.content;
    646             }
    647             if (hasContent)
    648                 this._drawOutlinedQuad(model.content, config.contentColor);
    649             this._context.restore();
    650 
    651             this._drawElementTitle();
    652 
    653             this._context.globalCompositeOperation = "destination-over";
    654         }
    655 
    656         this._context.drawImage(this._imageElement, 0, this._screenOffsetTop * this._screenZoom, this._imageElement.naturalWidth * this._imageZoom, this._imageElement.naturalHeight * this._imageZoom);
    657 
    658         this._context.restore();
    659     },
    660 
    661 
    662     /**
    663      * @param {!DOMAgent.Quad} quad1
    664      * @param {!DOMAgent.Quad} quad2
    665      * @return {boolean}
    666      */
    667     _quadsAreEqual: function(quad1, quad2)
    668     {
    669         for (var i = 0; i < quad1.length; ++i) {
    670             if (quad1[i] !== quad2[i])
    671                 return false;
    672         }
    673         return true;
    674     },
    675 
    676     /**
    677      * @param {!DOMAgent.RGBA} color
    678      * @return {string}
    679      */
    680     _cssColor: function(color)
    681     {
    682         if (!color)
    683             return "transparent";
    684         return WebInspector.Color.fromRGBA([color.r, color.g, color.b, color.a]).toString(WebInspector.Color.Format.RGBA) || "";
    685     },
    686 
    687     /**
    688      * @param {!DOMAgent.Quad} quad
    689      * @return {!CanvasRenderingContext2D}
    690      */
    691     _quadToPath: function(quad)
    692     {
    693         this._context.beginPath();
    694         this._context.moveTo(quad[0], quad[1]);
    695         this._context.lineTo(quad[2], quad[3]);
    696         this._context.lineTo(quad[4], quad[5]);
    697         this._context.lineTo(quad[6], quad[7]);
    698         this._context.closePath();
    699         return this._context;
    700     },
    701 
    702     /**
    703      * @param {!DOMAgent.Quad} quad
    704      * @param {!DOMAgent.RGBA} fillColor
    705      */
    706     _drawOutlinedQuad: function(quad, fillColor)
    707     {
    708         this._context.save();
    709         this._context.lineWidth = 2;
    710         this._quadToPath(quad).clip();
    711         this._context.fillStyle = this._cssColor(fillColor);
    712         this._context.fill();
    713         this._context.restore();
    714     },
    715 
    716     /**
    717      * @param {!DOMAgent.Quad} quad
    718      * @param {!DOMAgent.Quad} clipQuad
    719      * @param {!DOMAgent.RGBA} fillColor
    720      */
    721     _drawOutlinedQuadWithClip: function (quad, clipQuad, fillColor)
    722     {
    723         this._context.fillStyle = this._cssColor(fillColor);
    724         this._context.save();
    725         this._context.lineWidth = 0;
    726         this._quadToPath(quad).fill();
    727         this._context.globalCompositeOperation = "destination-out";
    728         this._context.fillStyle = "red";
    729         this._quadToPath(clipQuad).fill();
    730         this._context.restore();
    731     },
    732 
    733     _drawElementTitle: function()
    734     {
    735         if (!this._node)
    736             return;
    737 
    738         var canvasWidth = this._canvasElement.offsetWidth;
    739         var canvasHeight = this._canvasElement.offsetHeight;
    740 
    741         var lowerCaseName = this._node.localName() || this._node.nodeName().toLowerCase();
    742         this._tagNameElement.textContent = lowerCaseName;
    743         this._nodeIdElement.textContent = this._node.getAttribute("id") ? "#" + this._node.getAttribute("id") : "";
    744         this._nodeIdElement.textContent = this._node.getAttribute("id") ? "#" + this._node.getAttribute("id") : "";
    745         var className = this._node.getAttribute("class");
    746         if (className && className.length > 50)
    747            className = className.substring(0, 50) + "\u2026";
    748         this._classNameElement.textContent = className || "";
    749         this._nodeWidthElement.textContent = this._model.width;
    750         this._nodeHeightElement.textContent = this._model.height;
    751 
    752         var marginQuad = this._model.margin;
    753         var titleWidth = this._titleElement.offsetWidth + 6;
    754         var titleHeight = this._titleElement.offsetHeight + 4;
    755 
    756         var anchorTop = this._model.margin[1];
    757         var anchorBottom = this._model.margin[7];
    758 
    759         const arrowHeight = 7;
    760         var renderArrowUp = false;
    761         var renderArrowDown = false;
    762 
    763         var boxX = Math.max(2, this._model.margin[0]);
    764         if (boxX + titleWidth > canvasWidth)
    765             boxX = canvasWidth - titleWidth - 2;
    766 
    767         var boxY;
    768         if (anchorTop > canvasHeight) {
    769             boxY = canvasHeight - titleHeight - arrowHeight;
    770             renderArrowDown = true;
    771         } else if (anchorBottom < 0) {
    772             boxY = arrowHeight;
    773             renderArrowUp = true;
    774         } else if (anchorBottom + titleHeight + arrowHeight < canvasHeight) {
    775             boxY = anchorBottom + arrowHeight - 4;
    776             renderArrowUp = true;
    777         } else if (anchorTop - titleHeight - arrowHeight > 0) {
    778             boxY = anchorTop - titleHeight - arrowHeight + 3;
    779             renderArrowDown = true;
    780         } else
    781             boxY = arrowHeight;
    782 
    783         this._context.save();
    784         this._context.translate(0.5, 0.5);
    785         this._context.beginPath();
    786         this._context.moveTo(boxX, boxY);
    787         if (renderArrowUp) {
    788             this._context.lineTo(boxX + 2 * arrowHeight, boxY);
    789             this._context.lineTo(boxX + 3 * arrowHeight, boxY - arrowHeight);
    790             this._context.lineTo(boxX + 4 * arrowHeight, boxY);
    791         }
    792         this._context.lineTo(boxX + titleWidth, boxY);
    793         this._context.lineTo(boxX + titleWidth, boxY + titleHeight);
    794         if (renderArrowDown) {
    795             this._context.lineTo(boxX + 4 * arrowHeight, boxY + titleHeight);
    796             this._context.lineTo(boxX + 3 * arrowHeight, boxY + titleHeight + arrowHeight);
    797             this._context.lineTo(boxX + 2 * arrowHeight, boxY + titleHeight);
    798         }
    799         this._context.lineTo(boxX, boxY + titleHeight);
    800         this._context.closePath();
    801         this._context.fillStyle = "rgb(255, 255, 194)";
    802         this._context.fill();
    803         this._context.strokeStyle = "rgb(128, 128, 128)";
    804         this._context.stroke();
    805 
    806         this._context.restore();
    807 
    808         this._titleElement.classList.remove("hidden");
    809         this._titleElement.style.top = (boxY + 3) + "px";
    810         this._titleElement.style.left = (boxX + 3) + "px";
    811     },
    812 
    813     /**
    814      * @return {!{width: number, height: number}}
    815      */
    816     _viewportDimensions: function()
    817     {
    818         const gutterSize = 30;
    819         const bordersSize = WebInspector.ScreencastView._bordersSize;
    820         return { width: this.element.offsetWidth - bordersSize - gutterSize,
    821                  height: this.element.offsetHeight - bordersSize - gutterSize - WebInspector.ScreencastView._navBarHeight};
    822     },
    823 
    824     /**
    825      * @param {boolean} enabled
    826      * @param {boolean} inspectUAShadowDOM
    827      * @param {!DOMAgent.HighlightConfig} config
    828      * @param {function(?Protocol.Error)=} callback
    829      */
    830     setInspectModeEnabled: function(enabled, inspectUAShadowDOM, config, callback)
    831     {
    832         this._inspectModeConfig = enabled ? config : null;
    833         if (callback)
    834             callback(null);
    835     },
    836 
    837     /**
    838      * @param {!CanvasRenderingContext2D} context
    839      */
    840     _createCheckerboardPattern: function(context)
    841     {
    842         var pattern = /** @type {!HTMLCanvasElement} */(document.createElement("canvas"));
    843         const size = 32;
    844         pattern.width = size * 2;
    845         pattern.height = size * 2;
    846         var pctx = pattern.getContext("2d");
    847 
    848         pctx.fillStyle = "rgb(195, 195, 195)";
    849         pctx.fillRect(0, 0, size * 2, size * 2);
    850 
    851         pctx.fillStyle = "rgb(225, 225, 225)";
    852         pctx.fillRect(0, 0, size, size);
    853         pctx.fillRect(size, size, size, size);
    854         return context.createPattern(pattern, "repeat");
    855     },
    856 
    857     _createNavigationBar: function()
    858     {
    859         this._navigationBar = this.element.createChild("div", "toolbar-background screencast-navigation");
    860         if (WebInspector.queryParam("hideNavigation"))
    861             this._navigationBar.classList.add("hidden");
    862 
    863         this._navigationBack = this._navigationBar.createChild("button", "back");
    864         this._navigationBack.disabled = true;
    865         this._navigationBack.addEventListener("click", this._navigateToHistoryEntry.bind(this, -1), false);
    866 
    867         this._navigationForward = this._navigationBar.createChild("button", "forward");
    868         this._navigationForward.disabled = true;
    869         this._navigationForward.addEventListener("click", this._navigateToHistoryEntry.bind(this, 1), false);
    870 
    871         this._navigationReload = this._navigationBar.createChild("button", "reload");
    872         this._navigationReload.addEventListener("click", this._navigateReload.bind(this), false);
    873 
    874         this._navigationUrl = this._navigationBar.createChild("input");
    875         this._navigationUrl.type = "text";
    876         this._navigationUrl.addEventListener('keyup', this._navigationUrlKeyUp.bind(this), true);
    877 
    878         this._navigationProgressBar = new WebInspector.ScreencastView.ProgressTracker(this._navigationBar.createChild("div", "progress"));
    879 
    880         this._requestNavigationHistory();
    881         WebInspector.resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.InspectedURLChanged, this._requestNavigationHistory, this);
    882     },
    883 
    884     _navigateToHistoryEntry: function(offset)
    885     {
    886         var newIndex = this._historyIndex + offset;
    887         if (newIndex < 0 || newIndex >= this._historyEntries.length)
    888           return;
    889         PageAgent.navigateToHistoryEntry(this._historyEntries[newIndex].id);
    890         this._requestNavigationHistory();
    891     },
    892 
    893     _navigateReload: function()
    894     {
    895         WebInspector.resourceTreeModel.reloadPage();
    896     },
    897 
    898     _navigationUrlKeyUp: function(event)
    899     {
    900         if (event.keyIdentifier != 'Enter')
    901             return;
    902         var url = this._navigationUrl.value;
    903         if (!url)
    904             return;
    905         if (!url.match(WebInspector.ScreencastView._HttpRegex))
    906             url = "http://" + url;
    907         PageAgent.navigate(url);
    908         this._canvasElement.focus();
    909     },
    910 
    911     _requestNavigationHistory: function()
    912     {
    913         PageAgent.getNavigationHistory(this._onNavigationHistory.bind(this));
    914     },
    915 
    916     _onNavigationHistory: function(error, currentIndex, entries)
    917     {
    918         if (error)
    919           return;
    920 
    921         this._historyIndex = currentIndex;
    922         this._historyEntries = entries;
    923 
    924         this._navigationBack.disabled = currentIndex == 0;
    925         this._navigationForward.disabled = currentIndex == (entries.length - 1);
    926 
    927         var url = entries[currentIndex].url;
    928         var match = url.match(WebInspector.ScreencastView._HttpRegex);
    929         if (match)
    930             url = match[1];
    931         InspectorFrontendHost.inspectedURLChanged(url);
    932         this._navigationUrl.value = url;
    933     },
    934 
    935     _focusNavigationBar: function()
    936     {
    937         this._navigationUrl.focus();
    938         this._navigationUrl.select();
    939         return true;
    940     },
    941 
    942   __proto__: WebInspector.VBox.prototype
    943 }
    944 
    945 /**
    946  * @param {!Element} element
    947  * @constructor
    948  */
    949 WebInspector.ScreencastView.ProgressTracker = function(element) {
    950     this._element = element;
    951 
    952     WebInspector.resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.MainFrameNavigated, this._onMainFrameNavigated, this);
    953     WebInspector.resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.Load, this._onLoad, this);
    954 
    955     WebInspector.networkManager.addEventListener(WebInspector.NetworkManager.EventTypes.RequestStarted, this._onRequestStarted, this);
    956     WebInspector.networkManager.addEventListener(WebInspector.NetworkManager.EventTypes.RequestFinished, this._onRequestFinished, this);
    957 };
    958 
    959 WebInspector.ScreencastView.ProgressTracker.prototype = {
    960     _onMainFrameNavigated: function()
    961     {
    962         this._requestIds = {};
    963         this._startedRequests = 0;
    964         this._finishedRequests = 0;
    965         this._maxDisplayedProgress = 0;
    966         this._updateProgress(0.1);  // Display first 10% on navigation start.
    967     },
    968 
    969     _onLoad: function()
    970     {
    971         delete this._requestIds;
    972         this._updateProgress(1);  // Display 100% progress on load, hide it in 0.5s.
    973         setTimeout(function() {
    974             if (!this._navigationProgressVisible())
    975                 this._displayProgress(0);
    976         }.bind(this), 500);
    977     },
    978 
    979     _navigationProgressVisible: function()
    980     {
    981         return !!this._requestIds;
    982     },
    983 
    984     _onRequestStarted: function(event)
    985     {
    986       if (!this._navigationProgressVisible())
    987           return;
    988       var request = /** @type {!WebInspector.NetworkRequest} */ (event.data);
    989       // Ignore long-living WebSockets for the sake of progress indicator, as we won't be waiting them anyway.
    990       if (request.type === WebInspector.resourceTypes.WebSocket)
    991           return;
    992       this._requestIds[request.requestId] = request;
    993       ++this._startedRequests;
    994     },
    995 
    996     _onRequestFinished: function(event)
    997     {
    998         if (!this._navigationProgressVisible())
    999             return;
   1000         var request = /** @type {!WebInspector.NetworkRequest} */ (event.data);
   1001         if (!(request.requestId in this._requestIds))
   1002             return;
   1003         ++this._finishedRequests;
   1004         setTimeout(function() {
   1005             this._updateProgress(this._finishedRequests / this._startedRequests * 0.9);  // Finished requests drive the progress up to 90%.
   1006         }.bind(this), 500);  // Delay to give the new requests time to start. This makes the progress smoother.
   1007     },
   1008 
   1009     _updateProgress: function(progress)
   1010     {
   1011         if (!this._navigationProgressVisible())
   1012           return;
   1013         if (this._maxDisplayedProgress >= progress)
   1014           return;
   1015         this._maxDisplayedProgress = progress;
   1016         this._displayProgress(progress);
   1017     },
   1018 
   1019     _displayProgress: function(progress)
   1020     {
   1021         this._element.style.width = (100 * progress) + "%";
   1022     }
   1023 };
   1024