Home | History | Annotate | Download | only in screencast
      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         this._canvasElement.addEventListener("blur", this._handleBlurEvent.bind(this), false);
     75 
     76         this._titleElement = this._canvasContainerElement.createChild("div", "screencast-element-title monospace hidden");
     77         this._tagNameElement = this._titleElement.createChild("span", "screencast-tag-name");
     78         this._nodeIdElement = this._titleElement.createChild("span", "screencast-node-id");
     79         this._classNameElement = this._titleElement.createChild("span", "screencast-class-name");
     80         this._titleElement.createTextChild(" ");
     81         this._nodeWidthElement = this._titleElement.createChild("span");
     82         this._titleElement.createChild("span", "screencast-px").textContent = "px";
     83         this._titleElement.createTextChild(" \u00D7 ");
     84         this._nodeHeightElement = this._titleElement.createChild("span");
     85         this._titleElement.createChild("span", "screencast-px").textContent = "px";
     86 
     87         this._imageElement = new Image();
     88         this._isCasting = false;
     89         this._context = this._canvasElement.getContext("2d");
     90         this._checkerboardPattern = this._createCheckerboardPattern(this._context);
     91 
     92         this._shortcuts = /** !Object.<number, function(Event=):boolean> */ ({});
     93         this._shortcuts[WebInspector.KeyboardShortcut.makeKey("l", WebInspector.KeyboardShortcut.Modifiers.Ctrl)] = this._focusNavigationBar.bind(this);
     94 
     95         WebInspector.resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.ScreencastFrame, this._screencastFrame, this);
     96         WebInspector.resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.ScreencastVisibilityChanged, this._screencastVisibilityChanged, this);
     97 
     98         WebInspector.profilingLock().addEventListener(WebInspector.Lock.Events.StateChanged, this._onProfilingStateChange, this);
     99         this._updateGlasspane();
    100     },
    101 
    102     wasShown: function()
    103     {
    104         this._startCasting();
    105     },
    106 
    107     willHide: function()
    108     {
    109         this._stopCasting();
    110     },
    111 
    112     _startCasting: function()
    113     {
    114         if (WebInspector.profilingLock().isAcquired())
    115             return;
    116         if (this._isCasting)
    117             return;
    118         this._isCasting = true;
    119 
    120         const maxImageDimension = 2048;
    121         var dimensions = this._viewportDimensions();
    122         if (dimensions.width < 0 || dimensions.height < 0) {
    123             this._isCasting = false;
    124             return;
    125         }
    126         dimensions.width *= window.devicePixelRatio;
    127         dimensions.height *= window.devicePixelRatio;
    128         this._target.pageAgent().startScreencast("jpeg", 80, Math.min(maxImageDimension, dimensions.width), Math.min(maxImageDimension, dimensions.height));
    129         this._target.domModel.setHighlighter(this);
    130     },
    131 
    132     _stopCasting: function()
    133     {
    134         if (!this._isCasting)
    135             return;
    136         this._isCasting = false;
    137         this._target.pageAgent().stopScreencast();
    138         this._target.domModel.setHighlighter(null);
    139     },
    140 
    141     /**
    142      * @param {!WebInspector.Event} event
    143      */
    144     _screencastFrame: function(event)
    145     {
    146         var metadata = /** type {PageAgent.ScreencastFrameMetadata} */(event.data.metadata);
    147         var base64Data = /** type {string} */(event.data.data);
    148         this._imageElement.src = "data:image/jpg;base64," + base64Data;
    149         this._pageScaleFactor = metadata.pageScaleFactor;
    150         this._screenOffsetTop = metadata.offsetTop;
    151         this._deviceWidth = metadata.deviceWidth;
    152         this._deviceHeight = metadata.deviceHeight;
    153         this._scrollOffsetX = metadata.scrollOffsetX;
    154         this._scrollOffsetY = metadata.scrollOffsetY;
    155 
    156         var deviceSizeRatio = metadata.deviceHeight / metadata.deviceWidth;
    157         var dimensionsCSS = this._viewportDimensions();
    158 
    159         this._imageZoom = Math.min(dimensionsCSS.width / this._imageElement.naturalWidth, dimensionsCSS.height / (this._imageElement.naturalWidth * deviceSizeRatio));
    160         this._viewportElement.classList.remove("hidden");
    161         var bordersSize = WebInspector.ScreencastView._bordersSize;
    162         if (this._imageZoom < 1.01 / window.devicePixelRatio)
    163             this._imageZoom = 1 / window.devicePixelRatio;
    164         this._screenZoom = this._imageElement.naturalWidth * this._imageZoom / metadata.deviceWidth;
    165         this._viewportElement.style.width = metadata.deviceWidth * this._screenZoom + bordersSize + "px";
    166         this._viewportElement.style.height = metadata.deviceHeight * this._screenZoom + bordersSize + "px";
    167 
    168         this.highlightDOMNode(this._highlightNode, this._highlightConfig);
    169     },
    170 
    171     _isGlassPaneActive: function()
    172     {
    173         return !this._glassPaneElement.classList.contains("hidden");
    174     },
    175 
    176     /**
    177      * @param {!WebInspector.Event} event
    178      */
    179     _screencastVisibilityChanged: function(event)
    180     {
    181         this._targetInactive = !event.data.visible;
    182         this._updateGlasspane();
    183     },
    184 
    185     /**
    186      * @param {!WebInspector.Event} event
    187      */
    188     _onProfilingStateChange: function(event)
    189     {
    190         if (WebInspector.profilingLock().isAcquired())
    191             this._stopCasting();
    192         else
    193             this._startCasting();
    194         this._updateGlasspane();
    195     },
    196 
    197     _updateGlasspane: function()
    198     {
    199         if (this._targetInactive) {
    200             this._glassPaneElement.textContent = WebInspector.UIString("The tab is inactive");
    201             this._glassPaneElement.classList.remove("hidden");
    202         } else if (WebInspector.profilingLock().isAcquired()) {
    203             this._glassPaneElement.textContent = WebInspector.UIString("Profiling in progress");
    204             this._glassPaneElement.classList.remove("hidden");
    205         } else {
    206             this._glassPaneElement.classList.add("hidden");
    207         }
    208     },
    209 
    210     /**
    211      * @param {!Event} event
    212      */
    213     _handleMouseEvent: function(event)
    214     {
    215         if (this._isGlassPaneActive()) {
    216           event.consume();
    217           return;
    218         }
    219 
    220         if (!this._pageScaleFactor)
    221             return;
    222 
    223         if (!this._inspectModeConfig || event.type === "mousewheel") {
    224             this._simulateTouchForMouseEvent(event);
    225             event.preventDefault();
    226             if (event.type === "mousedown")
    227                 this._canvasElement.focus();
    228             return;
    229         }
    230 
    231         var position = this._convertIntoScreenSpace(event);
    232         this._target.domModel.nodeForLocation(position.x / this._pageScaleFactor + this._scrollOffsetX, position.y / this._pageScaleFactor + this._scrollOffsetY, callback.bind(this));
    233 
    234         /**
    235          * @param {?WebInspector.DOMNode} node
    236          * @this {WebInspector.ScreencastView}
    237          */
    238         function callback(node)
    239         {
    240             if (!node)
    241                 return;
    242             if (event.type === "mousemove")
    243                 this.highlightDOMNode(node, this._inspectModeConfig);
    244             else if (event.type === "click")
    245                 WebInspector.Revealer.reveal(node);
    246         }
    247     },
    248 
    249     /**
    250      * @param {!Event} event
    251      */
    252     _handleKeyEvent: function(event)
    253     {
    254         if (this._isGlassPaneActive()) {
    255             event.consume();
    256             return;
    257         }
    258 
    259         var shortcutKey = WebInspector.KeyboardShortcut.makeKeyFromEvent(/** @type {!KeyboardEvent} */ (event));
    260         var handler = this._shortcuts[shortcutKey];
    261         if (handler && handler(event)) {
    262             event.consume();
    263             return;
    264         }
    265 
    266         var type;
    267         switch (event.type) {
    268         case "keydown": type = "keyDown"; break;
    269         case "keyup": type = "keyUp"; break;
    270         case "keypress": type = "char"; break;
    271         default: return;
    272         }
    273 
    274         var text = event.type === "keypress" ? String.fromCharCode(event.charCode) : undefined;
    275         InputAgent.dispatchKeyEvent(type, this._modifiersForEvent(event), event.timeStamp / 1000, text, text ? text.toLowerCase() : undefined,
    276                                     event.keyIdentifier, event.keyCode /* windowsVirtualKeyCode */, event.keyCode /* nativeVirtualKeyCode */, false, false, false);
    277         event.consume();
    278         this._canvasElement.focus();
    279     },
    280 
    281     /**
    282      * @param {!Event} event
    283      */
    284     _handleContextMenuEvent: function(event)
    285     {
    286         event.consume(true);
    287     },
    288 
    289     /**
    290      * @param {!Event} event
    291      */
    292     _simulateTouchForMouseEvent: function(event)
    293     {
    294         const buttons = {0: "none", 1: "left", 2: "middle", 3: "right"};
    295         const types = {"mousedown" : "mousePressed", "mouseup": "mouseReleased", "mousemove": "mouseMoved", "mousewheel": "mouseWheel"};
    296         if (!(event.type in types) || !(event.which in buttons))
    297             return;
    298         if (event.type !== "mousewheel" && buttons[event.which] === "none")
    299             return;
    300 
    301         if (event.type === "mousedown" || typeof this._eventScreenOffsetTop === "undefined")
    302             this._eventScreenOffsetTop = this._screenOffsetTop;
    303 
    304         var modifiers = (event.altKey ? 1 : 0) | (event.ctrlKey ? 2 : 0) | (event.metaKey ? 4 : 0) | (event.shiftKey ? 8 : 0);
    305 
    306         var convertedPosition = this._zoomIntoScreenSpace(event);
    307         convertedPosition.y = Math.round(convertedPosition.y - this._eventScreenOffsetTop);
    308         var params = {type: types[event.type], x: convertedPosition.x, y: convertedPosition.y, modifiers: modifiers, timestamp: event.timeStamp / 1000, button: buttons[event.which], clickCount: 0};
    309         if (event.type === "mousewheel") {
    310             params.deltaX = event.wheelDeltaX / this._screenZoom;
    311             params.deltaY = event.wheelDeltaY / this._screenZoom;
    312         } else {
    313             this._eventParams = params;
    314         }
    315         if (event.type === "mouseup")
    316             delete this._eventScreenOffsetTop;
    317         InputAgent.invoke_emulateTouchFromMouseEvent(params);
    318     },
    319 
    320     /**
    321      * @param {!Event} event
    322      */
    323     _handleBlurEvent: function(event)
    324     {
    325         if (typeof this._eventScreenOffsetTop !== "undefined") {
    326             var params = this._eventParams;
    327             delete this._eventParams;
    328             params.type = "mouseReleased";
    329             InputAgent.invoke_emulateTouchFromMouseEvent(params);
    330         }
    331     },
    332 
    333     /**
    334      * @param {!Event} event
    335      * @return {!{x: number, y: number}}
    336      */
    337     _zoomIntoScreenSpace: function(event)
    338     {
    339         var position  = {};
    340         position.x = Math.round(event.offsetX / this._screenZoom);
    341         position.y = Math.round(event.offsetY / this._screenZoom);
    342         return position;
    343     },
    344 
    345     /**
    346      * @param {!Event} event
    347      * @return {!{x: number, y: number}}
    348      */
    349     _convertIntoScreenSpace: function(event)
    350     {
    351         var position = this._zoomIntoScreenSpace(event);
    352         position.y = Math.round(position.y - this._screenOffsetTop);
    353         return position;
    354     },
    355 
    356     /**
    357      * @param {!Event} event
    358      * @return {number}
    359      */
    360     _modifiersForEvent: function(event)
    361     {
    362         var modifiers = 0;
    363         if (event.altKey)
    364             modifiers = 1;
    365         if (event.ctrlKey)
    366             modifiers += 2;
    367         if (event.metaKey)
    368             modifiers += 4;
    369         if (event.shiftKey)
    370             modifiers += 8;
    371         return modifiers;
    372     },
    373 
    374     onResize: function()
    375     {
    376         if (this._deferredCasting) {
    377             clearTimeout(this._deferredCasting);
    378             delete this._deferredCasting;
    379         }
    380 
    381         this._stopCasting();
    382         this._deferredCasting = setTimeout(this._startCasting.bind(this), 100);
    383     },
    384 
    385     /**
    386      * @param {?WebInspector.DOMNode} node
    387      * @param {?DOMAgent.HighlightConfig} config
    388      * @param {!RuntimeAgent.RemoteObjectId=} objectId
    389      */
    390     highlightDOMNode: function(node, config, objectId)
    391     {
    392         this._highlightNode = node;
    393         this._highlightConfig = config;
    394         if (!node) {
    395             this._model = null;
    396             this._config = null;
    397             this._node = null;
    398             this._titleElement.classList.add("hidden");
    399             this._repaint();
    400             return;
    401         }
    402 
    403         this._node = node;
    404         node.boxModel(callback.bind(this));
    405 
    406         /**
    407          * @param {?DOMAgent.BoxModel} model
    408          * @this {WebInspector.ScreencastView}
    409          */
    410         function callback(model)
    411         {
    412             if (!model || !this._pageScaleFactor) {
    413                 this._repaint();
    414                 return;
    415             }
    416             this._model = this._scaleModel(model);
    417             this._config = config;
    418             this._repaint();
    419         }
    420     },
    421 
    422     /**
    423      * @param {!DOMAgent.BoxModel} model
    424      * @return {!DOMAgent.BoxModel}
    425      */
    426     _scaleModel: function(model)
    427     {
    428         /**
    429          * @param {!DOMAgent.Quad} quad
    430          * @this {WebInspector.ScreencastView}
    431          */
    432         function scaleQuad(quad)
    433         {
    434             for (var i = 0; i < quad.length; i += 2) {
    435                 quad[i] = quad[i] * this._pageScaleFactor * this._screenZoom;
    436                 quad[i + 1] = (quad[i + 1] * this._pageScaleFactor + this._screenOffsetTop) * this._screenZoom;
    437             }
    438         }
    439 
    440         scaleQuad.call(this, model.content);
    441         scaleQuad.call(this, model.padding);
    442         scaleQuad.call(this, model.border);
    443         scaleQuad.call(this, model.margin);
    444         return model;
    445     },
    446 
    447     _repaint: function()
    448     {
    449         var model = this._model;
    450         var config = this._config;
    451 
    452         var canvasWidth = this._canvasElement.getBoundingClientRect().width;
    453         var canvasHeight = this._canvasElement.getBoundingClientRect().height;
    454         this._canvasElement.width = window.devicePixelRatio * canvasWidth;
    455         this._canvasElement.height = window.devicePixelRatio * canvasHeight;
    456 
    457         this._context.save();
    458         this._context.scale(window.devicePixelRatio, window.devicePixelRatio);
    459 
    460         // Paint top and bottom gutter.
    461         this._context.save();
    462         this._context.fillStyle = this._checkerboardPattern;
    463         this._context.fillRect(0, 0, canvasWidth, this._screenOffsetTop * this._screenZoom);
    464         this._context.fillRect(0, this._screenOffsetTop * this._screenZoom + this._imageElement.naturalHeight * this._imageZoom, canvasWidth, canvasHeight);
    465         this._context.restore();
    466 
    467         if (model && config) {
    468             this._context.save();
    469             const transparentColor = "rgba(0, 0, 0, 0)";
    470             var hasContent = model.content && config.contentColor !== transparentColor;
    471             var hasPadding = model.padding && config.paddingColor !== transparentColor;
    472             var hasBorder = model.border && config.borderColor !== transparentColor;
    473             var hasMargin = model.margin && config.marginColor !== transparentColor;
    474 
    475             var clipQuad;
    476             if (hasMargin && (!hasBorder || !this._quadsAreEqual(model.margin, model.border))) {
    477                 this._drawOutlinedQuadWithClip(model.margin, model.border, config.marginColor);
    478                 clipQuad = model.border;
    479             }
    480             if (hasBorder && (!hasPadding || !this._quadsAreEqual(model.border, model.padding))) {
    481                 this._drawOutlinedQuadWithClip(model.border, model.padding, config.borderColor);
    482                 clipQuad = model.padding;
    483             }
    484             if (hasPadding && (!hasContent || !this._quadsAreEqual(model.padding, model.content))) {
    485                 this._drawOutlinedQuadWithClip(model.padding, model.content, config.paddingColor);
    486                 clipQuad = model.content;
    487             }
    488             if (hasContent)
    489                 this._drawOutlinedQuad(model.content, config.contentColor);
    490             this._context.restore();
    491 
    492             this._drawElementTitle();
    493 
    494             this._context.globalCompositeOperation = "destination-over";
    495         }
    496 
    497         this._context.drawImage(this._imageElement, 0, this._screenOffsetTop * this._screenZoom, this._imageElement.naturalWidth * this._imageZoom, this._imageElement.naturalHeight * this._imageZoom);
    498         this._context.restore();
    499 
    500     },
    501 
    502 
    503     /**
    504      * @param {!DOMAgent.Quad} quad1
    505      * @param {!DOMAgent.Quad} quad2
    506      * @return {boolean}
    507      */
    508     _quadsAreEqual: function(quad1, quad2)
    509     {
    510         for (var i = 0; i < quad1.length; ++i) {
    511             if (quad1[i] !== quad2[i])
    512                 return false;
    513         }
    514         return true;
    515     },
    516 
    517     /**
    518      * @param {!DOMAgent.RGBA} color
    519      * @return {string}
    520      */
    521     _cssColor: function(color)
    522     {
    523         if (!color)
    524             return "transparent";
    525         return WebInspector.Color.fromRGBA([color.r, color.g, color.b, color.a]).toString(WebInspector.Color.Format.RGBA) || "";
    526     },
    527 
    528     /**
    529      * @param {!DOMAgent.Quad} quad
    530      * @return {!CanvasRenderingContext2D}
    531      */
    532     _quadToPath: function(quad)
    533     {
    534         this._context.beginPath();
    535         this._context.moveTo(quad[0], quad[1]);
    536         this._context.lineTo(quad[2], quad[3]);
    537         this._context.lineTo(quad[4], quad[5]);
    538         this._context.lineTo(quad[6], quad[7]);
    539         this._context.closePath();
    540         return this._context;
    541     },
    542 
    543     /**
    544      * @param {!DOMAgent.Quad} quad
    545      * @param {!DOMAgent.RGBA} fillColor
    546      */
    547     _drawOutlinedQuad: function(quad, fillColor)
    548     {
    549         this._context.save();
    550         this._context.lineWidth = 2;
    551         this._quadToPath(quad).clip();
    552         this._context.fillStyle = this._cssColor(fillColor);
    553         this._context.fill();
    554         this._context.restore();
    555     },
    556 
    557     /**
    558      * @param {!DOMAgent.Quad} quad
    559      * @param {!DOMAgent.Quad} clipQuad
    560      * @param {!DOMAgent.RGBA} fillColor
    561      */
    562     _drawOutlinedQuadWithClip: function (quad, clipQuad, fillColor)
    563     {
    564         this._context.fillStyle = this._cssColor(fillColor);
    565         this._context.save();
    566         this._context.lineWidth = 0;
    567         this._quadToPath(quad).fill();
    568         this._context.globalCompositeOperation = "destination-out";
    569         this._context.fillStyle = "red";
    570         this._quadToPath(clipQuad).fill();
    571         this._context.restore();
    572     },
    573 
    574     _drawElementTitle: function()
    575     {
    576         if (!this._node)
    577             return;
    578 
    579         var canvasWidth = this._canvasElement.getBoundingClientRect().width;
    580         var canvasHeight = this._canvasElement.getBoundingClientRect().height;
    581 
    582         var lowerCaseName = this._node.localName() || this._node.nodeName().toLowerCase();
    583         this._tagNameElement.textContent = lowerCaseName;
    584         this._nodeIdElement.textContent = this._node.getAttribute("id") ? "#" + this._node.getAttribute("id") : "";
    585         this._nodeIdElement.textContent = this._node.getAttribute("id") ? "#" + this._node.getAttribute("id") : "";
    586         var className = this._node.getAttribute("class");
    587         if (className && className.length > 50)
    588            className = className.substring(0, 50) + "\u2026";
    589         this._classNameElement.textContent = className || "";
    590         this._nodeWidthElement.textContent = this._model.width;
    591         this._nodeHeightElement.textContent = this._model.height;
    592 
    593         var marginQuad = this._model.margin;
    594         var titleWidth = this._titleElement.offsetWidth + 6;
    595         var titleHeight = this._titleElement.offsetHeight + 4;
    596 
    597         var anchorTop = this._model.margin[1];
    598         var anchorBottom = this._model.margin[7];
    599 
    600         const arrowHeight = 7;
    601         var renderArrowUp = false;
    602         var renderArrowDown = false;
    603 
    604         var boxX = Math.max(2, this._model.margin[0]);
    605         if (boxX + titleWidth > canvasWidth)
    606             boxX = canvasWidth - titleWidth - 2;
    607 
    608         var boxY;
    609         if (anchorTop > canvasHeight) {
    610             boxY = canvasHeight - titleHeight - arrowHeight;
    611             renderArrowDown = true;
    612         } else if (anchorBottom < 0) {
    613             boxY = arrowHeight;
    614             renderArrowUp = true;
    615         } else if (anchorBottom + titleHeight + arrowHeight < canvasHeight) {
    616             boxY = anchorBottom + arrowHeight - 4;
    617             renderArrowUp = true;
    618         } else if (anchorTop - titleHeight - arrowHeight > 0) {
    619             boxY = anchorTop - titleHeight - arrowHeight + 3;
    620             renderArrowDown = true;
    621         } else
    622             boxY = arrowHeight;
    623 
    624         this._context.save();
    625         this._context.translate(0.5, 0.5);
    626         this._context.beginPath();
    627         this._context.moveTo(boxX, boxY);
    628         if (renderArrowUp) {
    629             this._context.lineTo(boxX + 2 * arrowHeight, boxY);
    630             this._context.lineTo(boxX + 3 * arrowHeight, boxY - arrowHeight);
    631             this._context.lineTo(boxX + 4 * arrowHeight, boxY);
    632         }
    633         this._context.lineTo(boxX + titleWidth, boxY);
    634         this._context.lineTo(boxX + titleWidth, boxY + titleHeight);
    635         if (renderArrowDown) {
    636             this._context.lineTo(boxX + 4 * arrowHeight, boxY + titleHeight);
    637             this._context.lineTo(boxX + 3 * arrowHeight, boxY + titleHeight + arrowHeight);
    638             this._context.lineTo(boxX + 2 * arrowHeight, boxY + titleHeight);
    639         }
    640         this._context.lineTo(boxX, boxY + titleHeight);
    641         this._context.closePath();
    642         this._context.fillStyle = "rgb(255, 255, 194)";
    643         this._context.fill();
    644         this._context.strokeStyle = "rgb(128, 128, 128)";
    645         this._context.stroke();
    646 
    647         this._context.restore();
    648 
    649         this._titleElement.classList.remove("hidden");
    650         this._titleElement.style.top = (boxY + 3) + "px";
    651         this._titleElement.style.left = (boxX + 3) + "px";
    652     },
    653 
    654     /**
    655      * @return {!{width: number, height: number}}
    656      */
    657     _viewportDimensions: function()
    658     {
    659         const gutterSize = 30;
    660         const bordersSize = WebInspector.ScreencastView._bordersSize;
    661         var width = this.element.offsetWidth - bordersSize - gutterSize;
    662         var height = this.element.offsetHeight - bordersSize - gutterSize - WebInspector.ScreencastView._navBarHeight;
    663         return { width: width, height: height };
    664     },
    665 
    666     /**
    667      * @param {boolean} enabled
    668      * @param {boolean} inspectUAShadowDOM
    669      * @param {!DOMAgent.HighlightConfig} config
    670      * @param {function(?Protocol.Error)=} callback
    671      */
    672     setInspectModeEnabled: function(enabled, inspectUAShadowDOM, config, callback)
    673     {
    674         this._inspectModeConfig = enabled ? config : null;
    675         if (callback)
    676             callback(null);
    677     },
    678 
    679     /**
    680      * @param {!CanvasRenderingContext2D} context
    681      */
    682     _createCheckerboardPattern: function(context)
    683     {
    684         var pattern = /** @type {!HTMLCanvasElement} */(document.createElement("canvas"));
    685         const size = 32;
    686         pattern.width = size * 2;
    687         pattern.height = size * 2;
    688         var pctx = pattern.getContext("2d");
    689 
    690         pctx.fillStyle = "rgb(195, 195, 195)";
    691         pctx.fillRect(0, 0, size * 2, size * 2);
    692 
    693         pctx.fillStyle = "rgb(225, 225, 225)";
    694         pctx.fillRect(0, 0, size, size);
    695         pctx.fillRect(size, size, size, size);
    696         return context.createPattern(pattern, "repeat");
    697     },
    698 
    699     _createNavigationBar: function()
    700     {
    701         this._navigationBar = this.element.createChild("div", "toolbar-background toolbar-colors screencast-navigation");
    702         if (Runtime.queryParam("hideNavigation"))
    703             this._navigationBar.classList.add("hidden");
    704 
    705         this._navigationBack = this._navigationBar.createChild("button", "back");
    706         this._navigationBack.disabled = true;
    707         this._navigationBack.addEventListener("click", this._navigateToHistoryEntry.bind(this, -1), false);
    708 
    709         this._navigationForward = this._navigationBar.createChild("button", "forward");
    710         this._navigationForward.disabled = true;
    711         this._navigationForward.addEventListener("click", this._navigateToHistoryEntry.bind(this, 1), false);
    712 
    713         this._navigationReload = this._navigationBar.createChild("button", "reload");
    714         this._navigationReload.addEventListener("click", this._navigateReload.bind(this), false);
    715 
    716         this._navigationUrl = this._navigationBar.createChild("input");
    717         this._navigationUrl.type = "text";
    718         this._navigationUrl.addEventListener('keyup', this._navigationUrlKeyUp.bind(this), true);
    719 
    720         this._navigationProgressBar = new WebInspector.ScreencastView.ProgressTracker(this._navigationBar.createChild("div", "progress"));
    721 
    722         this._requestNavigationHistory();
    723         WebInspector.targetManager.addEventListener(WebInspector.TargetManager.Events.InspectedURLChanged, this._requestNavigationHistory, this);
    724     },
    725 
    726     _navigateToHistoryEntry: function(offset)
    727     {
    728         var newIndex = this._historyIndex + offset;
    729         if (newIndex < 0 || newIndex >= this._historyEntries.length)
    730           return;
    731         PageAgent.navigateToHistoryEntry(this._historyEntries[newIndex].id);
    732         this._requestNavigationHistory();
    733     },
    734 
    735     _navigateReload: function()
    736     {
    737         WebInspector.resourceTreeModel.reloadPage();
    738     },
    739 
    740     _navigationUrlKeyUp: function(event)
    741     {
    742         if (event.keyIdentifier != 'Enter')
    743             return;
    744         var url = this._navigationUrl.value;
    745         if (!url)
    746             return;
    747         if (!url.match(WebInspector.ScreencastView._HttpRegex))
    748             url = "http://" + url;
    749         PageAgent.navigate(url);
    750         this._canvasElement.focus();
    751     },
    752 
    753     _requestNavigationHistory: function()
    754     {
    755         PageAgent.getNavigationHistory(this._onNavigationHistory.bind(this));
    756     },
    757 
    758     _onNavigationHistory: function(error, currentIndex, entries)
    759     {
    760         if (error)
    761           return;
    762 
    763         this._historyIndex = currentIndex;
    764         this._historyEntries = entries;
    765 
    766         this._navigationBack.disabled = currentIndex == 0;
    767         this._navigationForward.disabled = currentIndex == (entries.length - 1);
    768 
    769         var url = entries[currentIndex].url;
    770         var match = url.match(WebInspector.ScreencastView._HttpRegex);
    771         if (match)
    772             url = match[1];
    773         InspectorFrontendHost.inspectedURLChanged(url);
    774         this._navigationUrl.value = url;
    775     },
    776 
    777     _focusNavigationBar: function()
    778     {
    779         this._navigationUrl.focus();
    780         this._navigationUrl.select();
    781         return true;
    782     },
    783 
    784   __proto__: WebInspector.VBox.prototype
    785 }
    786 
    787 /**
    788  * @param {!Element} element
    789  * @constructor
    790  */
    791 WebInspector.ScreencastView.ProgressTracker = function(element)
    792 {
    793     this._element = element;
    794 
    795     WebInspector.targetManager.addModelListener(WebInspector.ResourceTreeModel, WebInspector.ResourceTreeModel.EventTypes.MainFrameNavigated, this._onMainFrameNavigated, this);
    796     WebInspector.targetManager.addModelListener(WebInspector.ResourceTreeModel, WebInspector.ResourceTreeModel.EventTypes.Load, this._onLoad, this);
    797     WebInspector.targetManager.addModelListener(WebInspector.NetworkManager, WebInspector.NetworkManager.EventTypes.RequestStarted, this._onRequestStarted, this);
    798     WebInspector.targetManager.addModelListener(WebInspector.NetworkManager, WebInspector.NetworkManager.EventTypes.RequestFinished, this._onRequestFinished, this);
    799 }
    800 
    801 WebInspector.ScreencastView.ProgressTracker.prototype = {
    802     _onMainFrameNavigated: function()
    803     {
    804         this._requestIds = {};
    805         this._startedRequests = 0;
    806         this._finishedRequests = 0;
    807         this._maxDisplayedProgress = 0;
    808         this._updateProgress(0.1);  // Display first 10% on navigation start.
    809     },
    810 
    811     _onLoad: function()
    812     {
    813         delete this._requestIds;
    814         this._updateProgress(1);  // Display 100% progress on load, hide it in 0.5s.
    815         setTimeout(function() {
    816             if (!this._navigationProgressVisible())
    817                 this._displayProgress(0);
    818         }.bind(this), 500);
    819     },
    820 
    821     _navigationProgressVisible: function()
    822     {
    823         return !!this._requestIds;
    824     },
    825 
    826     _onRequestStarted: function(event)
    827     {
    828       if (!this._navigationProgressVisible())
    829           return;
    830       var request = /** @type {!WebInspector.NetworkRequest} */ (event.data);
    831       // Ignore long-living WebSockets for the sake of progress indicator, as we won't be waiting them anyway.
    832       if (request.type === WebInspector.resourceTypes.WebSocket)
    833           return;
    834       this._requestIds[request.requestId] = request;
    835       ++this._startedRequests;
    836     },
    837 
    838     _onRequestFinished: function(event)
    839     {
    840         if (!this._navigationProgressVisible())
    841             return;
    842         var request = /** @type {!WebInspector.NetworkRequest} */ (event.data);
    843         if (!(request.requestId in this._requestIds))
    844             return;
    845         ++this._finishedRequests;
    846         setTimeout(function() {
    847             this._updateProgress(this._finishedRequests / this._startedRequests * 0.9);  // Finished requests drive the progress up to 90%.
    848         }.bind(this), 500);  // Delay to give the new requests time to start. This makes the progress smoother.
    849     },
    850 
    851     _updateProgress: function(progress)
    852     {
    853         if (!this._navigationProgressVisible())
    854           return;
    855         if (this._maxDisplayedProgress >= progress)
    856           return;
    857         this._maxDisplayedProgress = progress;
    858         this._displayProgress(progress);
    859     },
    860 
    861     _displayProgress: function(progress)
    862     {
    863         this._element.style.width = (100 * progress) + "%";
    864     }
    865 };
    866