| /* |
| * Copyright (C) 2013 Google Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions are |
| * met: |
| * |
| * * Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * * Redistributions in binary form must reproduce the above |
| * copyright notice, this list of conditions and the following disclaimer |
| * in the documentation and/or other materials provided with the |
| * distribution. |
| * * Neither the name of Google Inc. nor the names of its |
| * contributors may be used to endorse or promote products derived from |
| * this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| /* eslint-disable rulesdir/no_underscored_properties */ |
| |
| import * as Common from '../core/common/common.js'; |
| import * as Host from '../core/host/host.js'; |
| import * as i18n from '../core/i18n/i18n.js'; |
| import * as SDK from '../core/sdk/sdk.js'; |
| import * as UI from '../ui/legacy/legacy.js'; |
| |
| import {InputModel} from './InputModel.js'; |
| |
| const UIStrings = { |
| /** |
| *@description Accessible alt text for the screencast canvas rendering of the debug target webpage |
| */ |
| screencastViewOfDebugTarget: 'Screencast view of debug target', |
| /** |
| *@description Glass pane element text content in Screencast View of the Remote Devices tab when toggling screencast |
| */ |
| theTabIsInactive: 'The tab is inactive', |
| /** |
| *@description Glass pane element text content in Screencast View of the Remote Devices tab when toggling screencast |
| */ |
| profilingInProgress: 'Profiling in progress', |
| /** |
| *@description Accessible text for the screencast back button |
| */ |
| back: 'back', |
| /** |
| *@description Accessible text for the screencast forward button |
| */ |
| forward: 'forward', |
| /** |
| *@description Accessible text for the screencast reload button |
| */ |
| reload: 'reload', |
| /** |
| *@description Accessible text for the address bar in screencast view |
| */ |
| addressBar: 'Address bar', |
| }; |
| const str_ = i18n.i18n.registerUIStrings('screencast/ScreencastView.ts', UIStrings); |
| const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); |
| |
| interface Point { |
| x: number; |
| y: number; |
| } |
| |
| export class ScreencastView extends UI.Widget.VBox implements SDK.OverlayModel.Highlighter { |
| _screenCaptureModel: SDK.ScreenCaptureModel.ScreenCaptureModel; |
| _domModel: SDK.DOMModel.DOMModel|null; |
| _overlayModel: SDK.OverlayModel.OverlayModel|null; |
| _resourceTreeModel: SDK.ResourceTreeModel.ResourceTreeModel|null; |
| _networkManager: SDK.NetworkManager.NetworkManager|null; |
| _inputModel: InputModel|null; |
| _shortcuts: {[x: number]: (arg0?: Event|undefined) => boolean}; |
| _scrollOffsetX: number; |
| _scrollOffsetY: number; |
| _screenZoom: number; |
| _screenOffsetTop: number; |
| _pageScaleFactor: number; |
| _imageElement!: HTMLImageElement; |
| _viewportElement!: HTMLElement; |
| _glassPaneElement!: HTMLElement; |
| _canvasElement!: HTMLCanvasElement; |
| _titleElement!: HTMLElement; |
| _context!: CanvasRenderingContext2D; |
| _imageZoom: number; |
| _tagNameElement!: HTMLElement; |
| _attributeElement!: HTMLElement; |
| _nodeWidthElement!: HTMLElement; |
| _nodeHeightElement!: HTMLElement; |
| _model!: Protocol.DOM.BoxModel|null; |
| _highlightConfig!: Protocol.Overlay.HighlightConfig|null; |
| _navigationUrl!: HTMLInputElement; |
| _navigationBack!: HTMLButtonElement; |
| _navigationForward!: HTMLButtonElement; |
| _canvasContainerElement?: HTMLElement; |
| _isCasting?: boolean; |
| _checkerboardPattern?: CanvasPattern|null; |
| _targetInactive?: boolean; |
| _deferredCasting?: number; |
| _highlightNode?: SDK.DOMModel.DOMNode|null; |
| _config?: Protocol.Overlay.HighlightConfig|null; |
| _node?: SDK.DOMModel.DOMNode|null; |
| _inspectModeConfig?: Protocol.Overlay.HighlightConfig|null; |
| _navigationBar?: HTMLElement; |
| _navigationReload?: HTMLElement; |
| _navigationProgressBar?: ProgressTracker; |
| _historyIndex?: number; |
| _historyEntries?: Protocol.Page.NavigationEntry[]; |
| constructor(screenCaptureModel: SDK.ScreenCaptureModel.ScreenCaptureModel) { |
| super(); |
| this._screenCaptureModel = screenCaptureModel; |
| this._domModel = screenCaptureModel.target().model(SDK.DOMModel.DOMModel); |
| this._overlayModel = screenCaptureModel.target().model(SDK.OverlayModel.OverlayModel); |
| this._resourceTreeModel = screenCaptureModel.target().model(SDK.ResourceTreeModel.ResourceTreeModel); |
| this._networkManager = screenCaptureModel.target().model(SDK.NetworkManager.NetworkManager); |
| this._inputModel = screenCaptureModel.target().model(InputModel); |
| |
| this.setMinimumSize(150, 150); |
| this.registerRequiredCSS('screencast/screencastView.css', {enableLegacyPatching: true}); |
| this._shortcuts = {} as { |
| [x: number]: (arg0?: Event|undefined) => boolean, |
| }; |
| this._scrollOffsetX = 0; |
| this._scrollOffsetY = 0; |
| this._screenZoom = 1; |
| this._screenOffsetTop = 0; |
| this._pageScaleFactor = 1; |
| this._imageZoom = 1; |
| } |
| |
| initialize(): void { |
| this.element.classList.add('screencast'); |
| |
| this._createNavigationBar(); |
| this._viewportElement = this.element.createChild('div', 'screencast-viewport hidden') as HTMLElement; |
| this._canvasContainerElement = |
| this._viewportElement.createChild('div', 'screencast-canvas-container') as HTMLElement; |
| this._glassPaneElement = |
| this._canvasContainerElement.createChild('div', 'screencast-glasspane fill hidden') as HTMLElement; |
| this._canvasElement = this._canvasContainerElement.createChild('canvas') as HTMLCanvasElement; |
| UI.ARIAUtils.setAccessibleName(this._canvasElement, i18nString(UIStrings.screencastViewOfDebugTarget)); |
| this._canvasElement.tabIndex = 0; |
| this._canvasElement.addEventListener('mousedown', this._handleMouseEvent.bind(this), false); |
| this._canvasElement.addEventListener('mouseup', this._handleMouseEvent.bind(this), false); |
| this._canvasElement.addEventListener('mousemove', this._handleMouseEvent.bind(this), false); |
| this._canvasElement.addEventListener('mousewheel', this._handleMouseEvent.bind(this), false); |
| this._canvasElement.addEventListener('click', this._handleMouseEvent.bind(this), false); |
| this._canvasElement.addEventListener('contextmenu', this._handleContextMenuEvent.bind(this), false); |
| this._canvasElement.addEventListener('keydown', this._handleKeyEvent.bind(this), false); |
| this._canvasElement.addEventListener('keyup', this._handleKeyEvent.bind(this), false); |
| this._canvasElement.addEventListener('keypress', this._handleKeyEvent.bind(this), false); |
| this._canvasElement.addEventListener('blur', this._handleBlurEvent.bind(this), false); |
| this._titleElement = this._canvasContainerElement.createChild( |
| 'div', 'screencast-element-title monospace hidden -theme-not-patched') as HTMLElement; |
| this._tagNameElement = this._titleElement.createChild('span', 'screencast-tag-name') as HTMLElement; |
| this._attributeElement = this._titleElement.createChild('span', 'screencast-attribute') as HTMLElement; |
| UI.UIUtils.createTextChild(this._titleElement, ' '); |
| const dimension = this._titleElement.createChild('span', 'screencast-dimension') as HTMLElement; |
| this._nodeWidthElement = dimension.createChild('span') as HTMLElement; |
| UI.UIUtils.createTextChild(dimension, ' × '); |
| this._nodeHeightElement = dimension.createChild('span') as HTMLElement; |
| this._titleElement.style.top = '0'; |
| this._titleElement.style.left = '0'; |
| |
| this._imageElement = new Image(); |
| this._isCasting = false; |
| this._context = this._canvasElement.getContext('2d') as CanvasRenderingContext2D; |
| this._checkerboardPattern = this._createCheckerboardPattern(this._context); |
| |
| this._shortcuts[UI.KeyboardShortcut.KeyboardShortcut.makeKey('l', UI.KeyboardShortcut.Modifiers.Ctrl)] = |
| this._focusNavigationBar.bind(this); |
| |
| SDK.SDKModel.TargetManager.instance().addEventListener( |
| SDK.SDKModel.Events.SuspendStateChanged, this._onSuspendStateChange, this); |
| this._updateGlasspane(); |
| } |
| |
| wasShown(): void { |
| this._startCasting(); |
| } |
| |
| willHide(): void { |
| this._stopCasting(); |
| } |
| |
| _startCasting(): void { |
| if (SDK.SDKModel.TargetManager.instance().allTargetsSuspended()) { |
| return; |
| } |
| if (this._isCasting) { |
| return; |
| } |
| this._isCasting = true; |
| |
| const maxImageDimension = 2048; |
| const dimensions = this._viewportDimensions(); |
| if (dimensions.width < 0 || dimensions.height < 0) { |
| this._isCasting = false; |
| return; |
| } |
| dimensions.width *= window.devicePixelRatio; |
| dimensions.height *= window.devicePixelRatio; |
| // Note: startScreencast width and height are expected to be integers so must be floored. |
| this._screenCaptureModel.startScreencast( |
| Protocol.Page.StartScreencastRequestFormat.Jpeg, 80, Math.floor(Math.min(maxImageDimension, dimensions.width)), |
| Math.floor(Math.min(maxImageDimension, dimensions.height)), undefined, this._screencastFrame.bind(this), |
| this._screencastVisibilityChanged.bind(this)); |
| for (const emulationModel of SDK.SDKModel.TargetManager.instance().models(SDK.EmulationModel.EmulationModel)) { |
| emulationModel.overrideEmulateTouch(true); |
| } |
| if (this._overlayModel) { |
| this._overlayModel.setHighlighter(this); |
| } |
| } |
| |
| _stopCasting(): void { |
| if (!this._isCasting) { |
| return; |
| } |
| this._isCasting = false; |
| this._screenCaptureModel.stopScreencast(); |
| for (const emulationModel of SDK.SDKModel.TargetManager.instance().models(SDK.EmulationModel.EmulationModel)) { |
| emulationModel.overrideEmulateTouch(false); |
| } |
| if (this._overlayModel) { |
| this._overlayModel.setHighlighter(null); |
| } |
| } |
| |
| _screencastFrame(base64Data: string, metadata: Protocol.Page.ScreencastFrameMetadata): void { |
| this._imageElement.onload = (): void => { |
| this._pageScaleFactor = metadata.pageScaleFactor; |
| this._screenOffsetTop = metadata.offsetTop; |
| this._scrollOffsetX = metadata.scrollOffsetX; |
| this._scrollOffsetY = metadata.scrollOffsetY; |
| |
| const deviceSizeRatio = metadata.deviceHeight / metadata.deviceWidth; |
| const dimensionsCSS = this._viewportDimensions(); |
| |
| this._imageZoom = Math.min( |
| dimensionsCSS.width / this._imageElement.naturalWidth, |
| dimensionsCSS.height / (this._imageElement.naturalWidth * deviceSizeRatio)); |
| this._viewportElement.classList.remove('hidden'); |
| const bordersSize = BORDERS_SIZE; |
| if (this._imageZoom < 1.01 / window.devicePixelRatio) { |
| this._imageZoom = 1 / window.devicePixelRatio; |
| } |
| this._screenZoom = this._imageElement.naturalWidth * this._imageZoom / metadata.deviceWidth; |
| this._viewportElement.style.width = metadata.deviceWidth * this._screenZoom + bordersSize + 'px'; |
| this._viewportElement.style.height = metadata.deviceHeight * this._screenZoom + bordersSize + 'px'; |
| |
| const data = this._highlightNode ? {node: this._highlightNode, selectorList: undefined} : {clear: true}; |
| this._updateHighlightInOverlayAndRepaint(data, this._highlightConfig); |
| }; |
| this._imageElement.src = 'data:image/jpg;base64,' + base64Data; |
| } |
| |
| _isGlassPaneActive(): boolean { |
| return !this._glassPaneElement.classList.contains('hidden'); |
| } |
| |
| _screencastVisibilityChanged(visible: boolean): void { |
| this._targetInactive = !visible; |
| this._updateGlasspane(); |
| } |
| |
| _onSuspendStateChange(_event: Common.EventTarget.EventTargetEvent): void { |
| if (SDK.SDKModel.TargetManager.instance().allTargetsSuspended()) { |
| this._stopCasting(); |
| } else { |
| this._startCasting(); |
| } |
| this._updateGlasspane(); |
| } |
| |
| _updateGlasspane(): void { |
| if (this._targetInactive) { |
| this._glassPaneElement.textContent = i18nString(UIStrings.theTabIsInactive); |
| this._glassPaneElement.classList.remove('hidden'); |
| } else if (SDK.SDKModel.TargetManager.instance().allTargetsSuspended()) { |
| this._glassPaneElement.textContent = i18nString(UIStrings.profilingInProgress); |
| this._glassPaneElement.classList.remove('hidden'); |
| } else { |
| this._glassPaneElement.classList.add('hidden'); |
| } |
| } |
| |
| async _handleMouseEvent(event: Event): Promise<void> { |
| if (this._isGlassPaneActive()) { |
| event.consume(); |
| return; |
| } |
| |
| if (!this._pageScaleFactor || !this._domModel) { |
| return; |
| } |
| |
| if (!this._inspectModeConfig || event.type === 'mousewheel') { |
| if (this._inputModel) { |
| this._inputModel.emitTouchFromMouseEvent(event, this._screenOffsetTop, this._screenZoom); |
| } |
| event.preventDefault(); |
| if (event.type === 'mousedown') { |
| this._canvasElement.focus(); |
| } |
| return; |
| } |
| |
| const position = this._convertIntoScreenSpace(event as MouseEvent); |
| |
| const node = await this._domModel.nodeForLocation( |
| Math.floor(position.x / this._pageScaleFactor + this._scrollOffsetX), |
| Math.floor(position.y / this._pageScaleFactor + this._scrollOffsetY), |
| Common.Settings.Settings.instance().moduleSetting('showUAShadowDOM').get()); |
| |
| if (!node) { |
| return; |
| } |
| |
| if (event.type === 'mousemove') { |
| this._updateHighlightInOverlayAndRepaint({node, selectorList: undefined}, this._inspectModeConfig); |
| this._domModel.overlayModel().nodeHighlightRequested({nodeId: node.id as number}); |
| } else if (event.type === 'click') { |
| this._domModel.overlayModel().inspectNodeRequested({backendNodeId: node.backendNodeId()}); |
| } |
| } |
| |
| _handleKeyEvent(event: Event): void { |
| if (this._isGlassPaneActive()) { |
| event.consume(); |
| return; |
| } |
| |
| const shortcutKey = UI.KeyboardShortcut.KeyboardShortcut.makeKeyFromEvent(event as KeyboardEvent); |
| const handler = this._shortcuts[shortcutKey]; |
| if (handler && handler(event)) { |
| event.consume(); |
| return; |
| } |
| |
| if (this._inputModel) { |
| this._inputModel.emitKeyEvent(event); |
| } |
| event.consume(); |
| this._canvasElement.focus(); |
| } |
| |
| _handleContextMenuEvent(event: Event): void { |
| event.consume(true); |
| } |
| |
| _handleBlurEvent(_event: Event): void { |
| if (this._inputModel) { |
| this._inputModel.cancelTouch(); |
| } |
| } |
| |
| _convertIntoScreenSpace(event: MouseEvent): Point { |
| return { |
| x: Math.round(event.offsetX / this._screenZoom), |
| y: Math.round(event.offsetY / this._screenZoom - this._screenOffsetTop), |
| }; |
| } |
| |
| onResize(): void { |
| if (this._deferredCasting) { |
| clearTimeout(this._deferredCasting); |
| delete this._deferredCasting; |
| } |
| |
| this._stopCasting(); |
| this._deferredCasting = window.setTimeout(this._startCasting.bind(this), 100); |
| } |
| |
| highlightInOverlay(data: SDK.OverlayModel.HighlightData, config: Protocol.Overlay.HighlightConfig|null): void { |
| this._updateHighlightInOverlayAndRepaint(data, config); |
| } |
| |
| async _updateHighlightInOverlayAndRepaint( |
| data: SDK.OverlayModel.HighlightData, config: Protocol.Overlay.HighlightConfig|null): Promise<void> { |
| let node: SDK.DOMModel.DOMNode|null = null; |
| if ('node' in data) { |
| node = data.node; |
| } |
| if (!node && 'deferredNode' in data) { |
| node = await data.deferredNode.resolvePromise(); |
| } |
| if (!node && 'object' in data) { |
| const domModel = data.object.runtimeModel().target().model(SDK.DOMModel.DOMModel); |
| if (domModel) { |
| node = await domModel.pushObjectAsNodeToFrontend(data.object); |
| } |
| } |
| |
| this._highlightNode = node; |
| this._highlightConfig = config; |
| if (!node) { |
| this._model = null; |
| this._config = null; |
| this._node = null; |
| this._titleElement.classList.add('hidden'); |
| this._repaint(); |
| return; |
| } |
| |
| this._node = node; |
| node.boxModel().then(model => { |
| if (!model || !this._pageScaleFactor) { |
| this._repaint(); |
| return; |
| } |
| this._model = this._scaleModel(model); |
| this._config = config; |
| this._repaint(); |
| }); |
| } |
| |
| _scaleModel(model: Protocol.DOM.BoxModel): Protocol.DOM.BoxModel { |
| function scaleQuad(this: ScreencastView, quad: Protocol.DOM.Quad): void { |
| for (let i = 0; i < quad.length; i += 2) { |
| quad[i] = quad[i] * this._pageScaleFactor * this._screenZoom; |
| quad[i + 1] = (quad[i + 1] * this._pageScaleFactor + this._screenOffsetTop) * this._screenZoom; |
| } |
| } |
| |
| scaleQuad.call(this, model.content); |
| scaleQuad.call(this, model.padding); |
| scaleQuad.call(this, model.border); |
| scaleQuad.call(this, model.margin); |
| return model; |
| } |
| |
| _repaint(): void { |
| const model = this._model; |
| const config = this._config; |
| |
| const canvasWidth = this._canvasElement.getBoundingClientRect().width; |
| const canvasHeight = this._canvasElement.getBoundingClientRect().height; |
| this._canvasElement.width = window.devicePixelRatio * canvasWidth; |
| this._canvasElement.height = window.devicePixelRatio * canvasHeight; |
| |
| this._context.save(); |
| this._context.scale(window.devicePixelRatio, window.devicePixelRatio); |
| |
| // Paint top and bottom gutter. |
| this._context.save(); |
| if (this._checkerboardPattern) { |
| this._context.fillStyle = this._checkerboardPattern; |
| } |
| this._context.fillRect(0, 0, canvasWidth, this._screenOffsetTop * this._screenZoom); |
| this._context.fillRect( |
| 0, this._screenOffsetTop * this._screenZoom + this._imageElement.naturalHeight * this._imageZoom, canvasWidth, |
| canvasHeight); |
| this._context.restore(); |
| |
| if (model && config) { |
| this._context.save(); |
| const quads = []; |
| const isTransparent = (color: Protocol.DOM.RGBA): boolean => Boolean(color.a && color.a === 0); |
| if (model.content && config.contentColor && !isTransparent(config.contentColor)) { |
| quads.push({quad: model.content, color: config.contentColor}); |
| } |
| if (model.padding && config.paddingColor && !isTransparent(config.paddingColor)) { |
| quads.push({quad: model.padding, color: config.paddingColor}); |
| } |
| if (model.border && config.borderColor && !isTransparent(config.borderColor)) { |
| quads.push({quad: model.border, color: config.borderColor}); |
| } |
| if (model.margin && config.marginColor && !isTransparent(config.marginColor)) { |
| quads.push({quad: model.margin, color: config.marginColor}); |
| } |
| |
| for (let i = quads.length - 1; i > 0; --i) { |
| this._drawOutlinedQuadWithClip(quads[i].quad, quads[i - 1].quad, quads[i].color); |
| } |
| if (quads.length > 0) { |
| this._drawOutlinedQuad(quads[0].quad, quads[0].color); |
| } |
| this._context.restore(); |
| |
| this._drawElementTitle(); |
| |
| this._context.globalCompositeOperation = 'destination-over'; |
| } |
| |
| this._context.drawImage( |
| this._imageElement, 0, this._screenOffsetTop * this._screenZoom, |
| this._imageElement.naturalWidth * this._imageZoom, this._imageElement.naturalHeight * this._imageZoom); |
| this._context.restore(); |
| } |
| |
| _cssColor(color: Protocol.DOM.RGBA): string { |
| if (!color) { |
| return 'transparent'; |
| } |
| return Common.Color.Color.fromRGBA([color.r, color.g, color.b, color.a !== undefined ? color.a : 1]) |
| .asString(Common.Color.Format.RGBA) || |
| ''; |
| } |
| |
| _quadToPath(quad: Protocol.DOM.Quad): CanvasRenderingContext2D { |
| this._context.beginPath(); |
| this._context.moveTo(quad[0], quad[1]); |
| this._context.lineTo(quad[2], quad[3]); |
| this._context.lineTo(quad[4], quad[5]); |
| this._context.lineTo(quad[6], quad[7]); |
| this._context.closePath(); |
| return this._context; |
| } |
| |
| _drawOutlinedQuad(quad: Protocol.DOM.Quad, fillColor: Protocol.DOM.RGBA): void { |
| this._context.save(); |
| this._context.lineWidth = 2; |
| this._quadToPath(quad).clip(); |
| this._context.fillStyle = this._cssColor(fillColor); |
| this._context.fill(); |
| this._context.restore(); |
| } |
| |
| _drawOutlinedQuadWithClip(quad: Protocol.DOM.Quad, clipQuad: Protocol.DOM.Quad, fillColor: Protocol.DOM.RGBA): void { |
| this._context.fillStyle = this._cssColor(fillColor); |
| this._context.save(); |
| this._context.lineWidth = 0; |
| this._quadToPath(quad).fill(); |
| this._context.globalCompositeOperation = 'destination-out'; |
| this._context.fillStyle = 'red'; |
| this._quadToPath(clipQuad).fill(); |
| this._context.restore(); |
| } |
| |
| _drawElementTitle(): void { |
| if (!this._node) { |
| return; |
| } |
| |
| const canvasWidth = this._canvasElement.getBoundingClientRect().width; |
| const canvasHeight = this._canvasElement.getBoundingClientRect().height; |
| |
| const lowerCaseName = this._node.localName() || this._node.nodeName().toLowerCase(); |
| this._tagNameElement.textContent = lowerCaseName; |
| |
| this._attributeElement.textContent = getAttributesForElementTitle(this._node); |
| this._nodeWidthElement.textContent = String(this._model ? this._model.width : 0); |
| this._nodeHeightElement.textContent = String(this._model ? this._model.height : 0); |
| |
| this._titleElement.classList.remove('hidden'); |
| const titleWidth = this._titleElement.offsetWidth + 6; |
| const titleHeight = this._titleElement.offsetHeight + 4; |
| |
| const anchorTop = this._model ? this._model.margin[1] : 0; |
| const anchorBottom = this._model ? this._model.margin[7] : 0; |
| |
| const arrowHeight = 7; |
| let renderArrowUp = false; |
| let renderArrowDown = false; |
| |
| let boxX = Math.max(2, this._model ? this._model.margin[0] : 0); |
| if (boxX + titleWidth > canvasWidth) { |
| boxX = canvasWidth - titleWidth - 2; |
| } |
| |
| let boxY; |
| if (anchorTop > canvasHeight) { |
| boxY = canvasHeight - titleHeight - arrowHeight; |
| renderArrowDown = true; |
| } else if (anchorBottom < 0) { |
| boxY = arrowHeight; |
| renderArrowUp = true; |
| } else if (anchorBottom + titleHeight + arrowHeight < canvasHeight) { |
| boxY = anchorBottom + arrowHeight - 4; |
| renderArrowUp = true; |
| } else if (anchorTop - titleHeight - arrowHeight > 0) { |
| boxY = anchorTop - titleHeight - arrowHeight + 3; |
| renderArrowDown = true; |
| } else { |
| boxY = arrowHeight; |
| } |
| |
| this._context.save(); |
| this._context.translate(0.5, 0.5); |
| this._context.beginPath(); |
| this._context.moveTo(boxX, boxY); |
| if (renderArrowUp) { |
| this._context.lineTo(boxX + 2 * arrowHeight, boxY); |
| this._context.lineTo(boxX + 3 * arrowHeight, boxY - arrowHeight); |
| this._context.lineTo(boxX + 4 * arrowHeight, boxY); |
| } |
| this._context.lineTo(boxX + titleWidth, boxY); |
| this._context.lineTo(boxX + titleWidth, boxY + titleHeight); |
| if (renderArrowDown) { |
| this._context.lineTo(boxX + 4 * arrowHeight, boxY + titleHeight); |
| this._context.lineTo(boxX + 3 * arrowHeight, boxY + titleHeight + arrowHeight); |
| this._context.lineTo(boxX + 2 * arrowHeight, boxY + titleHeight); |
| } |
| this._context.lineTo(boxX, boxY + titleHeight); |
| this._context.closePath(); |
| this._context.fillStyle = 'rgb(255, 255, 194)'; |
| this._context.fill(); |
| this._context.strokeStyle = 'rgb(128, 128, 128)'; |
| this._context.stroke(); |
| |
| this._context.restore(); |
| |
| this._titleElement.style.top = (boxY + 3) + 'px'; |
| this._titleElement.style.left = (boxX + 3) + 'px'; |
| } |
| |
| _viewportDimensions(): {width: number, height: number} { |
| const gutterSize = 30; |
| const bordersSize = BORDERS_SIZE; |
| const width = this.element.offsetWidth - bordersSize - gutterSize; |
| const height = this.element.offsetHeight - bordersSize - gutterSize - NAVBAR_HEIGHT; |
| return {width: width, height: height}; |
| } |
| |
| setInspectMode(mode: Protocol.Overlay.InspectMode, config: Protocol.Overlay.HighlightConfig): Promise<void> { |
| this._inspectModeConfig = mode !== Protocol.Overlay.InspectMode.None ? config : null; |
| return Promise.resolve(); |
| } |
| |
| highlightFrame(_frameId: string): void { |
| } |
| |
| _createCheckerboardPattern(context: CanvasRenderingContext2D): CanvasPattern|null { |
| const pattern = document.createElement('canvas') as HTMLCanvasElement; |
| const size = 32; |
| pattern.width = size * 2; |
| pattern.height = size * 2; |
| const pctx = pattern.getContext('2d') as CanvasRenderingContext2D; |
| |
| pctx.fillStyle = 'rgb(195, 195, 195)'; |
| pctx.fillRect(0, 0, size * 2, size * 2); |
| |
| pctx.fillStyle = 'rgb(225, 225, 225)'; |
| pctx.fillRect(0, 0, size, size); |
| pctx.fillRect(size, size, size, size); |
| return context.createPattern(pattern, 'repeat'); |
| } |
| |
| _createNavigationBar(): void { |
| this._navigationBar = this.element.createChild('div', 'screencast-navigation') as HTMLElement; |
| this._navigationBack = this._navigationBar.createChild('button', 'back') as HTMLButtonElement; |
| this._navigationBack.disabled = true; |
| UI.ARIAUtils.setAccessibleName(this._navigationBack, i18nString(UIStrings.back)); |
| this._navigationForward = this._navigationBar.createChild('button', 'forward') as HTMLButtonElement; |
| this._navigationForward.disabled = true; |
| UI.ARIAUtils.setAccessibleName(this._navigationForward, i18nString(UIStrings.forward)); |
| this._navigationReload = this._navigationBar.createChild('button', 'reload'); |
| UI.ARIAUtils.setAccessibleName(this._navigationReload, i18nString(UIStrings.reload)); |
| this._navigationUrl = UI.UIUtils.createInput() as HTMLInputElement; |
| UI.ARIAUtils.setAccessibleName(this._navigationUrl, i18nString(UIStrings.addressBar)); |
| this._navigationBar.appendChild(this._navigationUrl); |
| this._navigationUrl.type = 'text'; |
| this._navigationProgressBar = new ProgressTracker( |
| this._resourceTreeModel, this._networkManager, |
| this._navigationBar.createChild('div', 'progress') as HTMLElement); |
| |
| if (this._resourceTreeModel) { |
| this._navigationBack.addEventListener('click', this._navigateToHistoryEntry.bind(this, -1), false); |
| this._navigationForward.addEventListener('click', this._navigateToHistoryEntry.bind(this, 1), false); |
| this._navigationReload.addEventListener('click', this._navigateReload.bind(this), false); |
| this._navigationUrl.addEventListener('keyup', this._navigationUrlKeyUp.bind(this), true); |
| this._requestNavigationHistory(); |
| this._resourceTreeModel.addEventListener( |
| SDK.ResourceTreeModel.Events.MainFrameNavigated, this._requestNavigationHistoryEvent, this); |
| this._resourceTreeModel.addEventListener( |
| SDK.ResourceTreeModel.Events.CachedResourcesLoaded, this._requestNavigationHistoryEvent, this); |
| } |
| } |
| |
| _navigateToHistoryEntry(offset: number): void { |
| if (!this._resourceTreeModel) { |
| return; |
| } |
| const newIndex = (this._historyIndex || 0) + offset; |
| if (!this._historyEntries || newIndex < 0 || newIndex >= this._historyEntries.length) { |
| return; |
| } |
| this._resourceTreeModel.navigateToHistoryEntry(this._historyEntries[newIndex]); |
| this._requestNavigationHistory(); |
| } |
| |
| _navigateReload(): void { |
| if (!this._resourceTreeModel) { |
| return; |
| } |
| this._resourceTreeModel.reloadPage(); |
| } |
| |
| _navigationUrlKeyUp(event: KeyboardEvent): void { |
| if (event.key !== 'Enter') { |
| return; |
| } |
| let url: string = this._navigationUrl.value; |
| if (!url) { |
| return; |
| } |
| if (!url.match(SCHEME_REGEX)) { |
| url = 'http://' + url; |
| } |
| |
| // Perform decodeURI in case the user enters an encoded string |
| // decodeURI has no effect on strings that are already decoded |
| // encodeURI ensures an encoded URL is always passed to the backend |
| // This allows the input field to support both encoded and decoded URLs |
| if (this._resourceTreeModel) { |
| this._resourceTreeModel.navigate(encodeURI(decodeURI(url))); |
| } |
| this._canvasElement.focus(); |
| } |
| |
| _requestNavigationHistoryEvent(_event: Common.EventTarget.EventTargetEvent): void { |
| this._requestNavigationHistory(); |
| } |
| |
| async _requestNavigationHistory(): Promise<void> { |
| const history = this._resourceTreeModel ? await this._resourceTreeModel.navigationHistory() : null; |
| if (!history) { |
| return; |
| } |
| |
| this._historyIndex = history.currentIndex; |
| this._historyEntries = history.entries; |
| |
| this._navigationBack.disabled = this._historyIndex === 0; |
| this._navigationForward.disabled = this._historyIndex === (this._historyEntries.length - 1); |
| |
| let url: string = this._historyEntries[this._historyIndex].url; |
| const match = url.match(HTTP_REGEX); |
| if (match) { |
| url = match[1]; |
| } |
| Host.InspectorFrontendHost.InspectorFrontendHostInstance.inspectedURLChanged(url); |
| this._navigationUrl.value = decodeURI(url); |
| } |
| |
| _focusNavigationBar(): boolean { |
| this._navigationUrl.focus(); |
| this._navigationUrl.select(); |
| return true; |
| } |
| } |
| |
| export const BORDERS_SIZE = 44; |
| export const NAVBAR_HEIGHT = 29; |
| export const HTTP_REGEX = /^http:\/\/(.+)/; |
| export const SCHEME_REGEX = /^(https?|about|chrome):/; |
| |
| export class ProgressTracker { |
| _element: HTMLElement; |
| _requestIds: Map<string, SDK.NetworkRequest.NetworkRequest>|null; |
| _startedRequests: number; |
| _finishedRequests: number; |
| _maxDisplayedProgress: number; |
| |
| constructor( |
| resourceTreeModel: SDK.ResourceTreeModel.ResourceTreeModel|null, |
| networkManager: SDK.NetworkManager.NetworkManager|null, element: HTMLElement) { |
| this._element = element; |
| if (resourceTreeModel) { |
| resourceTreeModel.addEventListener( |
| SDK.ResourceTreeModel.Events.MainFrameNavigated, this._onMainFrameNavigated, this); |
| resourceTreeModel.addEventListener(SDK.ResourceTreeModel.Events.Load, this._onLoad, this); |
| } |
| if (networkManager) { |
| networkManager.addEventListener(SDK.NetworkManager.Events.RequestStarted, this._onRequestStarted, this); |
| networkManager.addEventListener(SDK.NetworkManager.Events.RequestFinished, this._onRequestFinished, this); |
| } |
| this._requestIds = null; |
| this._startedRequests = 0; |
| this._finishedRequests = 0; |
| this._maxDisplayedProgress = 0; |
| } |
| |
| _onMainFrameNavigated(): void { |
| this._requestIds = new Map(); |
| this._startedRequests = 0; |
| this._finishedRequests = 0; |
| this._maxDisplayedProgress = 0; |
| this._updateProgress(0.1); // Display first 10% on navigation start. |
| } |
| |
| _onLoad(): void { |
| this._requestIds = null; |
| this._updateProgress(1); // Display 100% progress on load, hide it in 0.5s. |
| setTimeout(() => { |
| if (!this._navigationProgressVisible()) { |
| this._displayProgress(0); |
| } |
| }, 500); |
| } |
| |
| _navigationProgressVisible(): boolean { |
| return this._requestIds !== null; |
| } |
| |
| _onRequestStarted(event: Common.EventTarget.EventTargetEvent): void { |
| if (!this._navigationProgressVisible()) { |
| return; |
| } |
| const request = event.data.request as SDK.NetworkRequest.NetworkRequest; |
| // Ignore long-living WebSockets for the sake of progress indicator, as we won't be waiting them anyway. |
| if (request.resourceType() === Common.ResourceType.resourceTypes.WebSocket) { |
| return; |
| } |
| if (this._requestIds) { |
| this._requestIds.set(request.requestId(), request); |
| } |
| ++this._startedRequests; |
| } |
| |
| _onRequestFinished(event: Common.EventTarget.EventTargetEvent): void { |
| if (!this._navigationProgressVisible()) { |
| return; |
| } |
| const request = event.data as SDK.NetworkRequest.NetworkRequest; |
| if (this._requestIds && !this._requestIds.has(request.requestId())) { |
| return; |
| } |
| ++this._finishedRequests; |
| setTimeout(() => { |
| this._updateProgress( |
| this._finishedRequests / this._startedRequests * 0.9); // Finished requests drive the progress up to 90%. |
| }, 500); // Delay to give the new requests time to start. This makes the progress smoother. |
| } |
| |
| _updateProgress(progress: number): void { |
| if (!this._navigationProgressVisible()) { |
| return; |
| } |
| if (this._maxDisplayedProgress >= progress) { |
| return; |
| } |
| this._maxDisplayedProgress = progress; |
| this._displayProgress(progress); |
| } |
| |
| _displayProgress(progress: number): void { |
| this._element.style.width = (100 * progress) + '%'; |
| } |
| } |
| |
| function getAttributesForElementTitle(node: SDK.DOMModel.DOMNode): string { |
| const id = node.getAttribute('id'); |
| const className = node.getAttribute('class'); |
| |
| let selector: string = id ? '#' + id : ''; |
| if (className) { |
| selector += '.' + className.trim().replace(/\s+/g, '.'); |
| } |
| |
| if (selector.length > 50) { |
| selector = selector.substring(0, 50) + '…'; |
| } |
| |
| return selector; |
| } |