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