| /* |
| * 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) + "%"; |
| } |
| }; |