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