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