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