text: /*
   * Copyright (C) 2013 Google Inc. All rights reserved.
   *
   * Redistribution and use in source and binary forms, with or without
   * modification, are permitted provided that the following conditions are
   * met:
   *
   *     * Redistributions of source code must retain the above copyright
   * notice, this list of conditions and the following disclaimer.
   *     * Redistributions in binary form must reproduce the above
   * copyright notice, this list of conditions and the following disclaimer
   * in the documentation and/or other materials provided with the
   * distribution.
   *     * Neither the name of Google Inc. nor the names of its
   * contributors may be used to endorse or promote products derived from
   * this software without specific prior written permission.
   *
   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
   * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
   * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
   * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
   * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
   * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
   * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
   * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
   * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
   * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   */
  
  /**
   * @constructor
   * @extends {WebInspector.VBox}
   * @implements {WebInspector.DOMNodeHighlighter}
   * @param {!WebInspector.Target} target
   */
  WebInspector.ScreencastView = function(target)
  {
      WebInspector.VBox.call(this);
      this._target = target;
  
      this.setMinimumSize(150, 150);
      this.registerRequiredCSS("screencastView.css");
  };
  
  WebInspector.ScreencastView._bordersSize = 44;
  
  WebInspector.ScreencastView._navBarHeight = 29;
  
  WebInspector.ScreencastView._HttpRegex = /^https?:\/\/(.+)/;
  
  WebInspector.ScreencastView.prototype = {
      initialize: function()
      {
          this.element.classList.add("screencast");
  
          this._createNavigationBar();
  
          this._viewportElement = this.element.createChild("div", "screencast-viewport hidden");
          this._canvasContainerElement = this._viewportElement.createChild("div", "screencast-canvas-container");
          this._glassPaneElement = this._canvasContainerElement.createChild("div", "screencast-glasspane hidden");
  
          this._canvasElement = this._canvasContainerElement.createChild("canvas");
          this._canvasElement.tabIndex = 1;
          this._canvasElement.addEventListener("mousedown", this._handleMouseEvent.bind(this), false);
          this._canvasElement.addEventListener("mouseup", this._handleMouseEvent.bind(this), false);
          this._canvasElement.addEventListener("mousemove", this._handleMouseEvent.bind(this), false);
          this._canvasElement.addEventListener("mousewheel", this._handleMouseEvent.bind(this), false);
          this._canvasElement.addEventListener("click", this._handleMouseEvent.bind(this), false);
          this._canvasElement.addEventListener("contextmenu", this._handleContextMenuEvent.bind(this), false);
          this._canvasElement.addEventListener("keydown", this._handleKeyEvent.bind(this), false);
          this._canvasElement.addEventListener("keyup", this._handleKeyEvent.bind(this), false);
          this._canvasElement.addEventListener("keypress", this._handleKeyEvent.bind(this), false);
  
          this._titleElement = this._canvasContainerElement.createChild("div", "screencast-element-title monospace hidden");
          this._tagNameElement = this._titleElement.createChild("span", "screencast-tag-name");
          this._nodeIdElement = this._titleElement.createChild("span", "screencast-node-id");
          this._classNameElement = this._titleElement.createChild("span", "screencast-class-name");
          this._titleElement.appendChild(document.createTextNode(" "));
          this._nodeWidthElement = this._titleElement.createChild("span");
          this._titleElement.createChild("span", "screencast-px").textContent = "px";
          this._titleElement.appendChild(document.createTextNode(" \u00D7 "));
          this._nodeHeightElement = this._titleElement.createChild("span");
          this._titleElement.createChild("span", "screencast-px").textContent = "px";
  
          this._imageElement = new Image();
          this._isCasting = false;
          this._context = this._canvasElement.getContext("2d");
          this._checkerboardPattern = this._createCheckerboardPattern(this._context);
  
          this._shortcuts = /** !Object.<number, function(Event=):boolean> */ ({});
          this._shortcuts[WebInspector.KeyboardShortcut.makeKey("l", WebInspector.KeyboardShortcut.Modifiers.Ctrl)] = this._focusNavigationBar.bind(this);
  
          WebInspector.resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.ScreencastFrame, this._screencastFrame, this);
          WebInspector.resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.ScreencastVisibilityChanged, this._screencastVisibilityChanged, this);
  
          WebInspector.timelineManager.addEventListener(WebInspector.TimelineManager.EventTypes.TimelineStarted, this._onTimeline.bind(this, true), this);
          WebInspector.timelineManager.addEventListener(WebInspector.TimelineManager.EventTypes.TimelineStopped, this._onTimeline.bind(this, false), this);
          this._timelineActive = WebInspector.timelineManager.isStarted();
  
          WebInspector.cpuProfilerModel.addEventListener(WebInspector.CPUProfilerModel.EventTypes.ProfileStarted, this._onProfiler.bind(this, true), this);
          WebInspector.cpuProfilerModel.addEventListener(WebInspector.CPUProfilerModel.EventTypes.ProfileStopped, this._onProfiler.bind(this, false), this);
          this._profilerActive = WebInspector.cpuProfilerModel.isRecordingProfile();
  
          this._updateGlasspane();
      },
  
      wasShown: function()
      {
          this._startCasting();
      },
  
      willHide: function()
      {
          this._stopCasting();
      },
  
      _startCasting: function()
      {
          if (this._timelineActive || this._profilerActive)
              return;
          if (this._isCasting)
              return;
          this._isCasting = true;
  
          const maxImageDimension = 1024;
          var dimensions = this._viewportDimensions();
          if (dimensions.width < 0 || dimensions.height < 0) {
              this._isCasting = false;
              return;
          }
          dimensions.width *= WebInspector.zoomManager.zoomFactor();
          dimensions.height *= WebInspector.zoomManager.zoomFactor();
          this._target.pageAgent().startScreencast("jpeg", 80, Math.min(maxImageDimension, dimensions.width), Math.min(maxImageDimension, dimensions.height));
          this._target.domModel.setHighlighter(this);
      },
  
      _stopCasting: function()
      {
          if (!this._isCasting)
              return;
          this._isCasting = false;
          this._target.pageAgent().stopScreencast();
          this._target.domModel.setHighlighter(null);
      },
  
      /**
       * @param {!WebInspector.Event} event
       */
      _screencastFrame: function(event)
      {
          var metadata = /** type {PageAgent.ScreencastFrameMetadata} */(event.data.metadata);
  
          if (!metadata.deviceScaleFactor) {
            console.log(event.data.data);
            return;
          }
  
          var base64Data = /** type {string} */(event.data.data);
          this._imageElement.src = "data:image/jpg;base64," + base64Data;
          this._deviceScaleFactor = metadata.deviceScaleFactor;
          this._pageScaleFactor = metadata.pageScaleFactor;
          this._viewport = metadata.viewport;
          if (!this._viewport)
              return;
          var offsetTop = metadata.offsetTop || 0;
          var offsetBottom = metadata.offsetBottom || 0;
  
          var screenWidthDIP = this._viewport.width * this._pageScaleFactor;
          var screenHeightDIP = this._viewport.height * this._pageScaleFactor + offsetTop + offsetBottom;
          this._screenOffsetTop = offsetTop;
          this._resizeViewport(screenWidthDIP, screenHeightDIP);
  
          this._imageZoom = this._imageElement.naturalWidth ? this._canvasElement.offsetWidth / this._imageElement.naturalWidth : 1;
          this.highlightDOMNode(this._highlightNode, this._highlightConfig);
      },
  
      _isGlassPaneActive: function()
      {
          return !this._glassPaneElement.classList.contains("hidden");
      },
  
      /**
       * @param {!WebInspector.Event} event
       */
      _screencastVisibilityChanged: function(event)
      {
          this._targetInactive = !event.data.visible;
          this._updateGlasspane();
      },
  
      /**
       * @param {boolean} on
       * @private
       */
      _onTimeline: function(on)
      {
          this._timelineActive = on;
          if (this._timelineActive)
              this._stopCasting();
          else
              this._startCasting();
          this._updateGlasspane();
      },
  
      /**
       * @param {boolean} on
       * @param {!WebInspector.Event} event
       * @private
       */
      _onProfiler: function(on, event) {
          this._profilerActive = on;
          if (this._profilerActive)
              this._stopCasting();
          else
              this._startCasting();
          this._updateGlasspane();
      },
  
      _updateGlasspane: function()
      {
          if (this._targetInactive) {
              this._glassPaneElement.textContent = WebInspector.UIString("The tab is inactive");
              this._glassPaneElement.classList.remove("hidden");
          } else if (this._timelineActive) {
              this._glassPaneElement.textContent = WebInspector.UIString("Timeline is active");
              this._glassPaneElement.classList.remove("hidden");
          } else if (this._profilerActive) {
              this._glassPaneElement.textContent = WebInspector.UIString("CPU profiler is active");
              this._glassPaneElement.classList.remove("hidden");
          } else {
              this._glassPaneElement.classList.add("hidden");
          }
      },
  
      /**
       * @param {number} screenWidthDIP
       * @param {number} screenHeightDIP
       */
      _resizeViewport: function(screenWidthDIP, screenHeightDIP)
      {
          var dimensions = this._viewportDimensions();
          this._screenZoom = Math.min(dimensions.width / screenWidthDIP, dimensions.height / screenHeightDIP);
  
          var bordersSize = WebInspector.ScreencastView._bordersSize;
          this._viewportElement.classList.remove("hidden");
          this._viewportElement.style.width = screenWidthDIP * this._screenZoom + bordersSize + "px";
          this._viewportElement.style.height = screenHeightDIP * this._screenZoom + bordersSize + "px";
      },
  
      /**
       * @param {?Event} event
       */
      _handleMouseEvent: function(event)
      {
          if (this._isGlassPaneActive()) {
            event.consume();
            return;
          }
  
          if (!this._viewport)
              return;
  
          if (!this._inspectModeConfig || event.type === "mousewheel") {
              this._simulateTouchGestureForMouseEvent(event);
              event.preventDefault();
              if (event.type === "mousedown")
                  this._canvasElement.focus();
              return;
          }
  
          var position = this._convertIntoScreenSpace(event);
          this._target.domModel.nodeForLocation(position.x / this._pageScaleFactor, position.y / this._pageScaleFactor, callback.bind(this));
  
          /**
           * @param {?WebInspector.DOMNode} node
           * @this {WebInspector.ScreencastView}
           */
          function callback(node)
          {
              if (!node)
                  return;
              if (event.type === "mousemove")
                  this.highlightDOMNode(node, this._inspectModeConfig);
              else if (event.type === "click")
                  node.reveal();
          }
      },
  
      /**
       * @param {?Event} event
       */
      _handleKeyEvent: function(event)
      {
          if (this._isGlassPaneActive()) {
              event.consume();
              return;
          }
  
          var shortcutKey = WebInspector.KeyboardShortcut.makeKeyFromEvent(/** @type {!KeyboardEvent} */ (event));
          var handler = this._shortcuts[shortcutKey];
          if (handler && handler(event)) {
              event.consume();
              return;
          }
  
          var type;
          switch (event.type) {
          case "keydown": type = "keyDown"; break;
          case "keyup": type = "keyUp"; break;
          case "keypress": type = "char"; break;
          default: return;
          }
  
          var text = event.type === "keypress" ? String.fromCharCode(event.charCode) : undefined;
          InputAgent.dispatchKeyEvent(type, this._modifiersForEvent(event), event.timeStamp / 1000, text, text ? text.toLowerCase() : undefined,
                                      event.keyIdentifier, event.keyCode /* windowsVirtualKeyCode */, event.keyCode /* nativeVirtualKeyCode */, false, false, false);
          event.consume();
          this._canvasElement.focus();
      },
  
      /**
       * @param {?Event} event
       */
      _handleContextMenuEvent: function(event)
      {
          event.consume(true);
      },
  
      /**
       * @param {?Event} event
       */
      _simulateTouchGestureForMouseEvent: function(event)
      {
          var convertedPosition = this._convertIntoScreenSpace(event);
          var zoomedPosition = this._zoomIntoScreenSpace(event);
          var timeStamp = event.timeStamp / 1000;
  
          /**
           * @this {!WebInspector.ScreencastView}
           */
          function clearPinch()
          {
              delete this._lastPinchAnchor;
              delete this._lastPinchZoomedY;
              delete this._lastPinchScale;
          }
  
          /**
           * @this {!WebInspector.ScreencastView}
           */
          function clearScroll()
          {
              delete this._lastScrollZoomedPosition;
          }
  
          /**
           * @this {!WebInspector.ScreencastView}
           */
          function scrollBegin()
          {
              InputAgent.dispatchGestureEvent("scrollBegin", convertedPosition.x, convertedPosition.y, timeStamp);
              this._lastScrollZoomedPosition = zoomedPosition;
              clearPinch.call(this);
          }
  
          /**
           * @this {!WebInspector.ScreencastView}
           */
          function scrollUpdate()
          {
              var dx = this._lastScrollZoomedPosition ? zoomedPosition.x - this._lastScrollZoomedPosition.x : 0;
              var dy = this._lastScrollZoomedPosition ? zoomedPosition.y - this._lastScrollZoomedPosition.y : 0;
              if (dx || dy) {
                  InputAgent.dispatchGestureEvent("scrollUpdate", convertedPosition.x, convertedPosition.y, timeStamp, dx, dy);
                  this._lastScrollZoomedPosition = zoomedPosition;
              }
          }
  
          /**
           * @this {!WebInspector.ScreencastView}
           */
          function scrollEnd()
          {
              if (this._lastScrollZoomedPosition) {
                  InputAgent.dispatchGestureEvent("scrollEnd", convertedPosition.x, convertedPosition.y, timeStamp);
                  clearScroll.call(this);
                  return true;
              }
              return false;
          }
  
          /**
           * @this {!WebInspector.ScreencastView}
           */
          function pinchBegin()
          {
              InputAgent.dispatchGestureEvent("pinchBegin", convertedPosition.x, convertedPosition.y, timeStamp);
              this._lastPinchAnchor = convertedPosition;
              this._lastPinchZoomedY = zoomedPosition.y;
              this._lastPinchScale = 1;
              clearScroll.call(this);
          }
  
          /**
           * @this {!WebInspector.ScreencastView}
           */
          function pinchUpdate()
          {
              var dy = this._lastPinchZoomedY ? this._lastPinchZoomedY - zoomedPosition.y : 0;
              if (dy) {
                  var scale = Math.exp(dy * 0.002);
                  InputAgent.dispatchGestureEvent("pinchUpdate", this._lastPinchAnchor.x, this._lastPinchAnchor.y, timeStamp, 0, 0, scale / this._lastPinchScale);
                  this._lastPinchScale = scale;
              }
          }
  
          /**
           * @this {!WebInspector.ScreencastView}
           */
          function pinchEnd()
          {
              if (this._lastPinchAnchor) {
                  InputAgent.dispatchGestureEvent("pinchEnd", this._lastPinchAnchor.x, this._lastPinchAnchor.y, timeStamp);
                  clearPinch.call(this);
                  return true;
              }
              return false;
          }
  
          switch (event.which) {
          case 1: // Left
              if (event.type === "mousedown") {
                  if (event.shiftKey) {
                      pinchBegin.call(this);
                  } else {
                      scrollBegin.call(this);
                  }
              } else if (event.type === "mousemove") {
                  if (event.shiftKey) {
                      if (scrollEnd.call(this))
                          pinchBegin.call(this);
                      pinchUpdate.call(this);
                  } else {
                      if (pinchEnd.call(this))
                          scrollBegin.call(this);
                      scrollUpdate.call(this);
                  }
              } else if (event.type === "mouseup") {
                  pinchEnd.call(this);
                  scrollEnd.call(this);
              } else if (event.type === "mousewheel") {
                  if (!this._lastPinchAnchor && !this._lastScrollZoomedPosition) {
                      if (event.shiftKey) {
                          var factor = 1.1;
                          var scale = event.wheelDeltaY < 0 ? 1 / factor : factor;
                          InputAgent.dispatchGestureEvent("pinchBegin", convertedPosition.x, convertedPosition.y, timeStamp);
                          InputAgent.dispatchGestureEvent("pinchUpdate", convertedPosition.x, convertedPosition.y, timeStamp, 0, 0, scale);
                          InputAgent.dispatchGestureEvent("pinchEnd", convertedPosition.x, convertedPosition.y, timeStamp);
                      } else {
                          InputAgent.dispatchGestureEvent("scrollBegin", convertedPosition.x, convertedPosition.y, timeStamp);
                          InputAgent.dispatchGestureEvent("scrollUpdate", convertedPosition.x, convertedPosition.y, timeStamp, event.wheelDeltaX, event.wheelDeltaY);
                          InputAgent.dispatchGestureEvent("scrollEnd", convertedPosition.x, convertedPosition.y, timeStamp);
                      }
                  }
              } else if (event.type === "click") {
                  if (!event.shiftKey) {
                      InputAgent.dispatchMouseEvent("mousePressed", convertedPosition.x, convertedPosition.y, 0, timeStamp, "left", 1, true);
                      InputAgent.dispatchMouseEvent("mouseReleased", convertedPosition.x, convertedPosition.y, 0, timeStamp, "left", 1, true);
                      // FIXME: migrate to tap once it dispatches clicks again.
                      // InputAgent.dispatchGestureEvent("tapDown", x, y, timeStamp);
                      // InputAgent.dispatchGestureEvent("tap", x, y, timeStamp);
                  }
              }
              break;
  
          case 2: // Middle
              if (event.type === "mousedown") {
                  InputAgent.dispatchGestureEvent("tapDown", convertedPosition.x, convertedPosition.y, timeStamp);
              } else if (event.type === "mouseup") {
                  InputAgent.dispatchGestureEvent("tap", convertedPosition.x, convertedPosition.y, timeStamp);
              }
              break;
  
          case 3: // Right
          case 0: // None
          default:
          }
      },
  
      /**
       * @param {?Event} event
       * @return {!{x: number, y: number}}
       */
      _zoomIntoScreenSpace: function(event)
      {
          var zoom = this._canvasElement.offsetWidth / this._viewport.width / this._pageScaleFactor;
          var position  = {};
          position.x = Math.round(event.offsetX / zoom);
          position.y = Math.round(event.offsetY / zoom);
          return position;
      },
  
      /**
       * @param {?Event} event
       * @return {!{x: number, y: number}}
       */
      _convertIntoScreenSpace: function(event)
      {
          var position = this._zoomIntoScreenSpace(event);
          position.y = Math.round(position.y - this._screenOffsetTop);
          return position;
      },
  
      /**
       * @param {?Event} event
       * @return {number}
       */
      _modifiersForEvent: function(event)
      {
          var modifiers = 0;
          if (event.altKey)
              modifiers = 1;
          if (event.ctrlKey)
              modifiers += 2;
          if (event.metaKey)
              modifiers += 4;
          if (event.shiftKey)
              modifiers += 8;
          return modifiers;
      },
  
      onResize: function()
      {
          if (this._deferredCasting) {
              clearTimeout(this._deferredCasting);
              delete this._deferredCasting;
          }
  
          this._stopCasting();
          this._deferredCasting = setTimeout(this._startCasting.bind(this), 100);
      },
  
      /**
       * @param {?WebInspector.DOMNode} node
       * @param {?DOMAgent.HighlightConfig} config
       * @param {!RuntimeAgent.RemoteObjectId=} objectId
       */
      highlightDOMNode: function(node, config, objectId)
      {
          this._highlightNode = node;
          this._highlightConfig = config;
          if (!node) {
              this._model = null;
              this._config = null;
              this._node = null;
              this._titleElement.classList.add("hidden");
              this._repaint();
              return;
          }
  
          this._node = node;
          node.boxModel(callback.bind(this));
  
          /**
           * @param {?DOMAgent.BoxModel} model
           * @this {WebInspector.ScreencastView}
           */
          function callback(model)
          {
              if (!model) {
                  this._repaint();
                  return;
              }
              this._model = this._scaleModel(model);
              this._config = config;
              this._repaint();
          }
      },
  
      /**
       * @param {!DOMAgent.BoxModel} model
       * @return {!DOMAgent.BoxModel}
       */
      _scaleModel: function(model)
      {
          var scale = this._canvasElement.offsetWidth / this._viewport.width;
  
          /**
           * @param {!DOMAgent.Quad} quad
           * @this {WebInspector.ScreencastView}
           */
          function scaleQuad(quad)
          {
              for (var i = 0; i < quad.length; i += 2) {
                  quad[i] = (quad[i] - this._viewport.x) * scale;
                  quad[i + 1] = (quad[i + 1] - this._viewport.y) * scale + this._screenOffsetTop * this._screenZoom;
              }
          }
  
          scaleQuad.call(this, model.content);
          scaleQuad.call(this, model.padding);
          scaleQuad.call(this, model.border);
          scaleQuad.call(this, model.margin);
          return model;
      },
  
      _repaint: function()
      {
          var model = this._model;
          var config = this._config;
  
          this._canvasElement.width = window.devicePixelRatio * this._canvasElement.offsetWidth;
          this._canvasElement.height = window.devicePixelRatio * this._canvasElement.offsetHeight;
  
          this._context.save();
          this._context.scale(window.devicePixelRatio, window.devicePixelRatio);
  
          // Paint top and bottom gutter.
          this._context.save();
          this._context.fillStyle = this._checkerboardPattern;
          this._context.fillRect(0, 0, this._canvasElement.offsetWidth, this._screenOffsetTop * this._screenZoom);
          this._context.fillRect(0, this._screenOffsetTop * this._screenZoom + this._imageElement.naturalHeight * this._imageZoom, this._canvasElement.offsetWidth, this._canvasElement.offsetHeight);
          this._context.restore();
  
          if (model && config) {
              this._context.save();
              const transparentColor = "rgba(0, 0, 0, 0)";
              var hasContent = model.content && config.contentColor !== transparentColor;
              var hasPadding = model.padding && config.paddingColor !== transparentColor;
              var hasBorder = model.border && config.borderColor !== transparentColor;
              var hasMargin = model.margin && config.marginColor !== transparentColor;
  
              var clipQuad;
              if (hasMargin && (!hasBorder || !this._quadsAreEqual(model.margin, model.border))) {
                  this._drawOutlinedQuadWithClip(model.margin, model.border, config.marginColor);
                  clipQuad = model.border;
              }
              if (hasBorder && (!hasPadding || !this._quadsAreEqual(model.border, model.padding))) {
                  this._drawOutlinedQuadWithClip(model.border, model.padding, config.borderColor);
                  clipQuad = model.padding;
              }
              if (hasPadding && (!hasContent || !this._quadsAreEqual(model.padding, model.content))) {
                  this._drawOutlinedQuadWithClip(model.padding, model.content, config.paddingColor);
                  clipQuad = model.content;
              }
              if (hasContent)
                  this._drawOutlinedQuad(model.content, config.contentColor);
              this._context.restore();
  
              this._drawElementTitle();
  
              this._context.globalCompositeOperation = "destination-over";
          }
  
          this._context.drawImage(this._imageElement, 0, this._screenOffsetTop * this._screenZoom, this._imageElement.naturalWidth * this._imageZoom, this._imageElement.naturalHeight * this._imageZoom);
  
          this._context.restore();
      },
  
  
      /**
       * @param {!DOMAgent.Quad} quad1
       * @param {!DOMAgent.Quad} quad2
       * @return {boolean}
       */
      _quadsAreEqual: function(quad1, quad2)
      {
          for (var i = 0; i < quad1.length; ++i) {
              if (quad1[i] !== quad2[i])
                  return false;
          }
          return true;
      },
  
      /**
       * @param {!DOMAgent.RGBA} color
       * @return {string}
       */
      _cssColor: function(color)
      {
          if (!color)
              return "transparent";
          return WebInspector.Color.fromRGBA([color.r, color.g, color.b, color.a]).toString(WebInspector.Color.Format.RGBA) || "";
      },
  
      /**
       * @param {!DOMAgent.Quad} quad
       * @return {!CanvasRenderingContext2D}
       */
      _quadToPath: function(quad)
      {
          this._context.beginPath();
          this._context.moveTo(quad[0], quad[1]);
          this._context.lineTo(quad[2], quad[3]);
          this._context.lineTo(quad[4], quad[5]);
          this._context.lineTo(quad[6], quad[7]);
          this._context.closePath();
          return this._context;
      },
  
      /**
       * @param {!DOMAgent.Quad} quad
       * @param {!DOMAgent.RGBA} fillColor
       */
      _drawOutlinedQuad: function(quad, fillColor)
      {
          this._context.save();
          this._context.lineWidth = 2;
          this._quadToPath(quad).clip();
          this._context.fillStyle = this._cssColor(fillColor);
          this._context.fill();
          this._context.restore();
      },
  
      /**
       * @param {!DOMAgent.Quad} quad
       * @param {!DOMAgent.Quad} clipQuad
       * @param {!DOMAgent.RGBA} fillColor
       */
      _drawOutlinedQuadWithClip: function (quad, clipQuad, fillColor)
      {
          this._context.fillStyle = this._cssColor(fillColor);
          this._context.save();
          this._context.lineWidth = 0;
          this._quadToPath(quad).fill();
          this._context.globalCompositeOperation = "destination-out";
          this._context.fillStyle = "red";
          this._quadToPath(clipQuad).fill();
          this._context.restore();
      },
  
      _drawElementTitle: function()
      {
          if (!this._node)
              return;
  
          var canvasWidth = this._canvasElement.offsetWidth;
          var canvasHeight = this._canvasElement.offsetHeight;
  
          var lowerCaseName = this._node.localName() || this._node.nodeName().toLowerCase();
          this._tagNameElement.textContent = lowerCaseName;
          this._nodeIdElement.textContent = this._node.getAttribute("id") ? "#" + this._node.getAttribute("id") : "";
          this._nodeIdElement.textContent = this._node.getAttribute("id") ? "#" + this._node.getAttribute("id") : "";
          var className = this._node.getAttribute("class");
          if (className && className.length > 50)
             className = className.substring(0, 50) + "\u2026";
          this._classNameElement.textContent = className || "";
          this._nodeWidthElement.textContent = this._model.width;
          this._nodeHeightElement.textContent = this._model.height;
  
          var marginQuad = this._model.margin;
          var titleWidth = this._titleElement.offsetWidth + 6;
          var titleHeight = this._titleElement.offsetHeight + 4;
  
          var anchorTop = this._model.margin[1];
          var anchorBottom = this._model.margin[7];
  
          const arrowHeight = 7;
          var renderArrowUp = false;
          var renderArrowDown = false;
  
          var boxX = Math.max(2, this._model.margin[0]);
          if (boxX + titleWidth > canvasWidth)
              boxX = canvasWidth - titleWidth - 2;
  
          var boxY;
          if (anchorTop > canvasHeight) {
              boxY = canvasHeight - titleHeight - arrowHeight;
              renderArrowDown = true;
          } else if (anchorBottom < 0) {
              boxY = arrowHeight;
              renderArrowUp = true;
          } else if (anchorBottom + titleHeight + arrowHeight < canvasHeight) {
              boxY = anchorBottom + arrowHeight - 4;
              renderArrowUp = true;
          } else if (anchorTop - titleHeight - arrowHeight > 0) {
              boxY = anchorTop - titleHeight - arrowHeight + 3;
              renderArrowDown = true;
          } else
              boxY = arrowHeight;
  
          this._context.save();
          this._context.translate(0.5, 0.5);
          this._context.beginPath();
          this._context.moveTo(boxX, boxY);
          if (renderArrowUp) {
              this._context.lineTo(boxX + 2 * arrowHeight, boxY);
              this._context.lineTo(boxX + 3 * arrowHeight, boxY - arrowHeight);
              this._context.lineTo(boxX + 4 * arrowHeight, boxY);
          }
          this._context.lineTo(boxX + titleWidth, boxY);
          this._context.lineTo(boxX + titleWidth, boxY + titleHeight);
          if (renderArrowDown) {
              this._context.lineTo(boxX + 4 * arrowHeight, boxY + titleHeight);
              this._context.lineTo(boxX + 3 * arrowHeight, boxY + titleHeight + arrowHeight);
              this._context.lineTo(boxX + 2 * arrowHeight, boxY + titleHeight);
          }
          this._context.lineTo(boxX, boxY + titleHeight);
          this._context.closePath();
          this._context.fillStyle = "rgb(255, 255, 194)";
          this._context.fill();
          this._context.strokeStyle = "rgb(128, 128, 128)";
          this._context.stroke();
  
          this._context.restore();
  
          this._titleElement.classList.remove("hidden");
          this._titleElement.style.top = (boxY + 3) + "px";
          this._titleElement.style.left = (boxX + 3) + "px";
      },
  
      /**
       * @return {!{width: number, height: number}}
       */
      _viewportDimensions: function()
      {
          const gutterSize = 30;
          const bordersSize = WebInspector.ScreencastView._bordersSize;
          return { width: this.element.offsetWidth - bordersSize - gutterSize,
                   height: this.element.offsetHeight - bordersSize - gutterSize - WebInspector.ScreencastView._navBarHeight};
      },
  
      /**
       * @param {boolean} enabled
       * @param {boolean} inspectUAShadowDOM
       * @param {!DOMAgent.HighlightConfig} config
       * @param {function(?Protocol.Error)=} callback
       */
      setInspectModeEnabled: function(enabled, inspectUAShadowDOM, config, callback)
      {
          this._inspectModeConfig = enabled ? config : null;
          if (callback)
              callback(null);
      },
  
      /**
       * @param {!CanvasRenderingContext2D} context
       */
      _createCheckerboardPattern: function(context)
      {
          var pattern = /** @type {!HTMLCanvasElement} */(document.createElement("canvas"));
          const size = 32;
          pattern.width = size * 2;
          pattern.height = size * 2;
          var pctx = pattern.getContext("2d");
  
          pctx.fillStyle = "rgb(195, 195, 195)";
          pctx.fillRect(0, 0, size * 2, size * 2);
  
          pctx.fillStyle = "rgb(225, 225, 225)";
          pctx.fillRect(0, 0, size, size);
          pctx.fillRect(size, size, size, size);
          return context.createPattern(pattern, "repeat");
      },
  
      _createNavigationBar: function()
      {
          this._navigationBar = this.element.createChild("div", "toolbar-background screencast-navigation");
          if (WebInspector.queryParam("hideNavigation"))
              this._navigationBar.classList.add("hidden");
  
          this._navigationBack = this._navigationBar.createChild("button", "back");
          this._navigationBack.disabled = true;
          this._navigationBack.addEventListener("click", this._navigateToHistoryEntry.bind(this, -1), false);
  
          this._navigationForward = this._navigationBar.createChild("button", "forward");
          this._navigationForward.disabled = true;
          this._navigationForward.addEventListener("click", this._navigateToHistoryEntry.bind(this, 1), false);
  
          this._navigationReload = this._navigationBar.createChild("button", "reload");
          this._navigationReload.addEventListener("click", this._navigateReload.bind(this), false);
  
          this._navigationUrl = this._navigationBar.createChild("input");
          this._navigationUrl.type = "text";
          this._navigationUrl.addEventListener('keyup', this._navigationUrlKeyUp.bind(this), true);
  
          this._navigationProgressBar = new WebInspector.ScreencastView.ProgressTracker(this._navigationBar.createChild("div", "progress"));
  
          this._requestNavigationHistory();
          WebInspector.resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.InspectedURLChanged, this._requestNavigationHistory, this);
      },
  
      _navigateToHistoryEntry: function(offset)
      {
          var newIndex = this._historyIndex + offset;
          if (newIndex < 0 || newIndex >= this._historyEntries.length)
            return;
          PageAgent.navigateToHistoryEntry(this._historyEntries[newIndex].id);
          this._requestNavigationHistory();
      },
  
      _navigateReload: function()
      {
          WebInspector.resourceTreeModel.reloadPage();
      },
  
      _navigationUrlKeyUp: function(event)
      {
          if (event.keyIdentifier != 'Enter')
              return;
          var url = this._navigationUrl.value;
          if (!url)
              return;
          if (!url.match(WebInspector.ScreencastView._HttpRegex))
              url = "http://" + url;
          PageAgent.navigate(url);
          this._canvasElement.focus();
      },
  
      _requestNavigationHistory: function()
      {
          PageAgent.getNavigationHistory(this._onNavigationHistory.bind(this));
      },
  
      _onNavigationHistory: function(error, currentIndex, entries)
      {
          if (error)
            return;
  
          this._historyIndex = currentIndex;
          this._historyEntries = entries;
  
          this._navigationBack.disabled = currentIndex == 0;
          this._navigationForward.disabled = currentIndex == (entries.length - 1);
  
          var url = entries[currentIndex].url;
          var match = url.match(WebInspector.ScreencastView._HttpRegex);
          if (match)
              url = match[1];
          InspectorFrontendHost.inspectedURLChanged(url);
          this._navigationUrl.value = url;
      },
  
      _focusNavigationBar: function()
      {
          this._navigationUrl.focus();
          this._navigationUrl.select();
          return true;
      },
  
    __proto__: WebInspector.VBox.prototype
  }
  
  /**
   * @param {!Element} element
   * @constructor
   */
  WebInspector.ScreencastView.ProgressTracker = function(element) {
      this._element = element;
  
      WebInspector.resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.MainFrameNavigated, this._onMainFrameNavigated, this);
      WebInspector.resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.Load, this._onLoad, this);
  
      WebInspector.networkManager.addEventListener(WebInspector.NetworkManager.EventTypes.RequestStarted, this._onRequestStarted, this);
      WebInspector.networkManager.addEventListener(WebInspector.NetworkManager.EventTypes.RequestFinished, this._onRequestFinished, this);
  };
  
  WebInspector.ScreencastView.ProgressTracker.prototype = {
      _onMainFrameNavigated: function()
      {
          this._requestIds = {};
          this._startedRequests = 0;
          this._finishedRequests = 0;
          this._maxDisplayedProgress = 0;
          this._updateProgress(0.1);  // Display first 10% on navigation start.
      },
  
      _onLoad: function()
      {
          delete this._requestIds;
          this._updateProgress(1);  // Display 100% progress on load, hide it in 0.5s.
          setTimeout(function() {
              if (!this._navigationProgressVisible())
                  this._displayProgress(0);
          }.bind(this), 500);
      },
  
      _navigationProgressVisible: function()
      {
          return !!this._requestIds;
      },
  
      _onRequestStarted: function(event)
      {
        if (!this._navigationProgressVisible())
            return;
        var request = /** @type {!WebInspector.NetworkRequest} */ (event.data);
        // Ignore long-living WebSockets for the sake of progress indicator, as we won't be waiting them anyway.
        if (request.type === WebInspector.resourceTypes.WebSocket)
            return;
        this._requestIds[request.requestId] = request;
        ++this._startedRequests;
      },
  
      _onRequestFinished: function(event)
      {
          if (!this._navigationProgressVisible())
              return;
          var request = /** @type {!WebInspector.NetworkRequest} */ (event.data);
          if (!(request.requestId in this._requestIds))
              return;
          ++this._finishedRequests;
          setTimeout(function() {
              this._updateProgress(this._finishedRequests / this._startedRequests * 0.9);  // Finished requests drive the progress up to 90%.
          }.bind(this), 500);  // Delay to give the new requests time to start. This makes the progress smoother.
      },
  
      _updateProgress: function(progress)
      {
          if (!this._navigationProgressVisible())
            return;
          if (this._maxDisplayedProgress >= progress)
            return;
          this._maxDisplayedProgress = progress;
          this._displayProgress(progress);
      },
  
      _displayProgress: function(progress)
      {
          this._element.style.width = (100 * progress) + "%";
      }
  };
  