| /* |
| * Copyright (C) 2017 Apple 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: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. 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. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS 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 APPLE INC. OR ITS 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. |
| */ |
| |
| WI.CanvasContentView = class CanvasContentView extends WI.ContentView |
| { |
| constructor(representedObject) |
| { |
| console.assert(representedObject instanceof WI.Canvas); |
| |
| super(representedObject); |
| |
| this.element.classList.add("canvas"); |
| |
| this._progressView = null; |
| this._previewContainerElement = null; |
| this._previewImageElement = null; |
| this._errorElement = null; |
| this._memoryCostElement = null; |
| this._pendingContent = null; |
| this._pixelSize = null; |
| this._pixelSizeElement = null; |
| this._canvasNode = null; |
| |
| this._refreshButtonNavigationItem = new WI.ButtonNavigationItem("refresh", WI.UIString("Refresh"), "Images/ReloadFull.svg", 13, 13); |
| this._refreshButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low; |
| this._refreshButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this.refreshPreview, this); |
| |
| this._showGridButtonNavigationItem = new WI.ActivateButtonNavigationItem("show-grid", WI.UIString("Show Grid"), WI.UIString("Hide Grid"), "Images/NavigationItemCheckers.svg", 13, 13); |
| this._showGridButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._showGridButtonClicked, this); |
| this._showGridButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low; |
| this._showGridButtonNavigationItem.activated = !!WI.settings.showImageGrid.value; |
| } |
| |
| // Public |
| |
| get navigationItems() |
| { |
| // The toggle recording NavigationItem isn't added to the ContentBrowser's NavigationBar. |
| // It's added to the "quick access" NavigationBar shown when hovering the canvas in the overview. |
| return [this._refreshButtonNavigationItem, this._showGridButtonNavigationItem]; |
| } |
| |
| refreshPreview() |
| { |
| this._pendingContent = null; |
| |
| this.representedObject.requestContent().then((content) => { |
| this._pendingContent = content; |
| if (!this._pendingContent) { |
| this._showError(); |
| return; |
| } |
| |
| this.needsLayout(); |
| }); |
| } |
| |
| // Protected |
| |
| initialLayout() |
| { |
| super.initialLayout(); |
| |
| let isCard = !this._refreshButtonNavigationItem.parentNavigationBar; |
| |
| if (isCard) { |
| let header = this.element.appendChild(document.createElement("header")); |
| header.addEventListener("click", (event) => { event.stopPropagation(); }); |
| |
| let titles = header.appendChild(document.createElement("div")); |
| titles.className = "titles"; |
| |
| let title = titles.appendChild(document.createElement("span")); |
| title.className = "title"; |
| title.textContent = this.representedObject.displayName; |
| |
| let subtitle = titles.appendChild(document.createElement("span")); |
| subtitle.className = "subtitle"; |
| subtitle.textContent = WI.Canvas.displayNameForContextType(this.representedObject.contextType); |
| |
| let navigationBar = new WI.NavigationBar; |
| |
| if (this.representedObject.contextType === WI.Canvas.ContextType.Canvas2D || this.representedObject.contextType === WI.Canvas.ContextType.BitmapRenderer || this.representedObject.contextType === WI.Canvas.ContextType.WebGL || this.representedObject.contextType === WI.Canvas.ContextType.WebGL2) { |
| const toolTip = WI.UIString("Start recording canvas actions.\nShift-click to record a single frame."); |
| const altToolTip = WI.UIString("Stop recording canvas actions"); |
| this._recordButtonNavigationItem = new WI.ToggleButtonNavigationItem("record-start-stop", toolTip, altToolTip, "Images/Record.svg", "Images/Stop.svg", 13, 13); |
| this._recordButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.High; |
| this._recordButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._toggleRecording, this); |
| navigationBar.addNavigationItem(this._recordButtonNavigationItem); |
| } |
| |
| let canvasElementButtonNavigationItem = new WI.ButtonNavigationItem("canvas-element", WI.UIString("Canvas Element"), "Images/Markup.svg", 16, 16); |
| canvasElementButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low; |
| canvasElementButtonNavigationItem.element.addEventListener("mousedown", this._handleCanvasElementButtonMouseDown.bind(this)); |
| navigationBar.addNavigationItem(canvasElementButtonNavigationItem); |
| |
| navigationBar.addNavigationItem(this._refreshButtonNavigationItem); |
| |
| header.append(navigationBar.element); |
| } |
| |
| this._previewContainerElement = this.element.appendChild(document.createElement("div")); |
| this._previewContainerElement.className = "preview"; |
| |
| if (isCard) { |
| let footer = this.element.appendChild(document.createElement("footer")); |
| footer.addEventListener("click", (event) => { event.stopPropagation(); }); |
| |
| this._viewRelatedItemsContainer = footer.appendChild(document.createElement("div")); |
| this._viewRelatedItemsContainer.classList.add("view-related-items"); |
| |
| this._viewShaderButton = document.createElement("img"); |
| this._viewShaderButton.classList.add("view-shader"); |
| this._viewShaderButton.title = WI.UIString("View Shader"); |
| this._viewShaderButton.addEventListener("mousedown", this._handleViewShaderButtonMouseDown.bind(this)); |
| |
| this._viewRecordingButton = document.createElement("img"); |
| this._viewRecordingButton.classList.add("view-recording"); |
| this._viewRecordingButton.title = WI.UIString("View Recording"); |
| this._viewRecordingButton.addEventListener("mousedown", this._handleViewRecordingButtonMouseDown.bind(this)); |
| |
| this._updateViewRelatedItems(); |
| |
| let flexibleSpaceElement = footer.appendChild(document.createElement("div")); |
| flexibleSpaceElement.className = "flexible-space"; |
| |
| let metrics = footer.appendChild(document.createElement("div")); |
| |
| this._pixelSizeElement = metrics.appendChild(document.createElement("span")); |
| this._pixelSizeElement.className = "pixel-size"; |
| |
| this._memoryCostElement = metrics.appendChild(document.createElement("span")); |
| this._memoryCostElement.className = "memory-cost"; |
| } |
| |
| if (this._errorElement) |
| this._showError(); |
| |
| if (isCard) |
| this._refreshPixelSize(); |
| } |
| |
| layout() |
| { |
| super.layout(); |
| |
| if (this._pendingContent) { |
| if (this._errorElement) { |
| this._errorElement.remove(); |
| this._errorElement = null; |
| } |
| |
| if (!this._previewImageElement) { |
| this._previewImageElement = document.createElement("img"); |
| this._previewImageElement.addEventListener("error", this._showError.bind(this)); |
| } |
| |
| this._previewImageElement.src = this._pendingContent; |
| this._pendingContent = null; |
| |
| if (!this._previewImageElement.parentNode) |
| this._previewContainerElement.appendChild(this._previewImageElement); |
| } |
| |
| this._updateRecordNavigationItem(); |
| this._updateProgressView(); |
| this._updateViewRelatedItems(); |
| this._updateMemoryCost(); |
| this._updateImageGrid(); |
| } |
| |
| shown() |
| { |
| super.shown(); |
| |
| this.refreshPreview(); |
| } |
| |
| attached() |
| { |
| super.attached(); |
| |
| this.representedObject.addEventListener(WI.Canvas.Event.MemoryChanged, this._updateMemoryCost, this); |
| this.representedObject.addEventListener(WI.Canvas.Event.RecordingStarted, this.needsLayout, this); |
| this.representedObject.addEventListener(WI.Canvas.Event.RecordingProgress, this.needsLayout, this); |
| this.representedObject.addEventListener(WI.Canvas.Event.RecordingStopped, this.needsLayout, this); |
| this.representedObject.shaderProgramCollection.addEventListener(WI.Collection.Event.ItemAdded, this.needsLayout, this); |
| this.representedObject.shaderProgramCollection.addEventListener(WI.Collection.Event.ItemRemoved, this.needsLayout, this); |
| |
| this.representedObject.requestNode().then((node) => { |
| console.assert(!this._canvasNode || this._canvasNode === node); |
| if (this._canvasNode === node) |
| return; |
| |
| this._canvasNode = node; |
| this._canvasNode.addEventListener(WI.DOMNode.Event.AttributeModified, this._refreshPixelSize, this); |
| this._canvasNode.addEventListener(WI.DOMNode.Event.AttributeRemoved, this._refreshPixelSize, this); |
| }); |
| |
| WI.settings.showImageGrid.addEventListener(WI.Setting.Event.Changed, this._updateImageGrid, this); |
| } |
| |
| detached() |
| { |
| this.representedObject.removeEventListener(null, null, this); |
| this.representedObject.shaderProgramCollection.removeEventListener(null, null, this); |
| |
| if (this._canvasNode) { |
| this._canvasNode.removeEventListener(null, null, this); |
| this._canvasNode = null; |
| } |
| |
| WI.settings.showImageGrid.removeEventListener(null, null, this); |
| |
| super.detached(); |
| } |
| |
| // Private |
| |
| _showError() |
| { |
| if (this._previewImageElement) |
| this._previewImageElement.remove(); |
| |
| if (!this._errorElement) { |
| const isError = true; |
| this._errorElement = WI.createMessageTextView(WI.UIString("No Preview Available"), isError); |
| } |
| |
| if (this._previewContainerElement) |
| this._previewContainerElement.appendChild(this._errorElement); |
| } |
| |
| _toggleRecording(event) |
| { |
| if (this.representedObject.recordingActive) |
| this.representedObject.stopRecording(); |
| else { |
| let singleFrame = event.data.nativeEvent.shiftKey; |
| this.representedObject.startRecording(singleFrame); |
| } |
| } |
| |
| _refreshPixelSize() |
| { |
| let updatePixelSize = (size) => { |
| if (Object.shallowEqual(this._pixelSize, size)) |
| return; |
| |
| this._pixelSize = size; |
| |
| if (this._pixelSizeElement) { |
| if (this._pixelSize) |
| this._pixelSizeElement.textContent = `${this._pixelSize.width} ${multiplicationSign} ${this._pixelSize.height}`; |
| else |
| this._pixelSizeElement.textContent = emDash; |
| } |
| |
| this.refreshPreview(); |
| }; |
| |
| this.representedObject.requestSize() |
| .then((size) => { |
| updatePixelSize(size); |
| }) |
| .catch((error) => { |
| updatePixelSize(null); |
| }); |
| } |
| |
| _handleCanvasElementButtonMouseDown(event) |
| { |
| if (this._ignoreCanvasElementButtonMouseDown) |
| return; |
| |
| this._ignoreCanvasElementButtonMouseDown = true; |
| |
| let contextMenu = WI.ContextMenu.createFromEvent(event); |
| contextMenu.addBeforeShowCallback(() => { |
| this._ignoreCanvasElementButtonMouseDown = false; |
| }); |
| |
| if (this._canvasNode) |
| WI.appendContextMenuItemsForDOMNode(contextMenu, this._canvasNode); |
| |
| contextMenu.appendSeparator(); |
| |
| contextMenu.appendItem(WI.UIString("Log Canvas Context"), () => { |
| WI.RemoteObject.resolveCanvasContext(this.representedObject, WI.RuntimeManager.ConsoleObjectGroup, (remoteObject) => { |
| if (!remoteObject) |
| return; |
| |
| const text = WI.UIString("Selected Canvas Context"); |
| const addSpecialUserLogClass = true; |
| WI.consoleLogViewController.appendImmediateExecutionWithResult(text, remoteObject, addSpecialUserLogClass); |
| }); |
| }); |
| |
| contextMenu.show(); |
| } |
| |
| _showGridButtonClicked() |
| { |
| WI.settings.showImageGrid.value = !this._showGridButtonNavigationItem.activated; |
| } |
| |
| _updateImageGrid() |
| { |
| let activated = WI.settings.showImageGrid.value; |
| this._showGridButtonNavigationItem.activated = activated; |
| |
| if (this._previewImageElement) |
| this._previewImageElement.classList.toggle("show-grid", activated); |
| } |
| |
| _updateMemoryCost() |
| { |
| if (!this._memoryCostElement) |
| return; |
| |
| let memoryCost = this.representedObject.memoryCost; |
| if (isNaN(memoryCost)) |
| this._memoryCostElement.textContent = emDash; |
| else { |
| const higherResolution = false; |
| let bytesString = Number.bytesToString(memoryCost, higherResolution); |
| this._memoryCostElement.textContent = `(${bytesString})`; |
| } |
| } |
| |
| _updateRecordNavigationItem() |
| { |
| if (!this._recordButtonNavigationItem) |
| return; |
| |
| let recordingActive = this.representedObject.recordingActive; |
| this._recordButtonNavigationItem.toggled = recordingActive; |
| this._refreshButtonNavigationItem.enabled = !recordingActive; |
| this.element.classList.toggle("recording-active", recordingActive); |
| } |
| |
| _updateProgressView() |
| { |
| if (!this._previewContainerElement) |
| return; |
| |
| if (!this.representedObject.recordingActive) { |
| if (this._progressView && this._progressView.parentView) { |
| this.removeSubview(this._progressView); |
| this._progressView = null; |
| } |
| return; |
| } |
| |
| if (!this._progressView) { |
| this._progressView = new WI.ProgressView; |
| this.element.insertBefore(this._progressView.element, this._previewContainerElement); |
| this.addSubview(this._progressView); |
| } |
| |
| let title = null; |
| if (this.representedObject.recordingFrameCount) { |
| let formatString = this.representedObject.recordingFrameCount === 1 ? WI.UIString("%d Frame") : WI.UIString("%d Frames"); |
| title = formatString.format(this.representedObject.recordingFrameCount); |
| } else |
| title = WI.UIString("Waiting for frames\u2026"); |
| |
| this._progressView.title = title; |
| this._progressView.subtitle = this.representedObject.recordingBufferUsed ? Number.bytesToString(this.representedObject.recordingBufferUsed) : ""; |
| } |
| |
| _updateViewRelatedItems() |
| { |
| if (!this._viewRelatedItemsContainer) |
| return; |
| |
| this._viewRelatedItemsContainer.removeChildren(); |
| |
| if (this.representedObject.shaderProgramCollection.size) |
| this._viewRelatedItemsContainer.appendChild(this._viewShaderButton); |
| |
| if (this.representedObject.recordingCollection.size) |
| this._viewRelatedItemsContainer.appendChild(this._viewRecordingButton); |
| } |
| |
| _handleViewShaderButtonMouseDown(event) |
| { |
| if (this._ignoreViewShaderButtonMouseDown) |
| return; |
| |
| let shaderPrograms = this.representedObject.shaderProgramCollection; |
| console.assert(shaderPrograms.size); |
| if (!shaderPrograms.size) |
| return; |
| |
| if (shaderPrograms.size === 1) { |
| WI.showRepresentedObject(Array.from(shaderPrograms)[0]); |
| return; |
| } |
| |
| this._ignoreViewShaderButtonMouseDown = true; |
| |
| let contextMenu = WI.ContextMenu.createFromEvent(event); |
| contextMenu.addBeforeShowCallback(() => { |
| this._ignoreViewShaderButtonMouseDown = false; |
| }); |
| |
| for (let shaderProgram of shaderPrograms) { |
| contextMenu.appendItem(shaderProgram.displayName, () => { |
| WI.showRepresentedObject(shaderProgram); |
| }); |
| } |
| |
| contextMenu.show(); |
| } |
| |
| _handleViewRecordingButtonMouseDown(event) |
| { |
| if (this._ignoreViewRecordingButtonMouseDown) |
| return; |
| |
| let recordings = this.representedObject.recordingCollection; |
| console.assert(recordings.size); |
| if (!recordings.size) |
| return; |
| |
| if (recordings.size === 1) { |
| WI.showRepresentedObject(Array.from(recordings)[0]); |
| return; |
| } |
| |
| this._ignoreViewRecordingButtonMouseDown = true; |
| |
| let contextMenu = WI.ContextMenu.createFromEvent(event); |
| contextMenu.addBeforeShowCallback(() => { |
| this._ignoreViewRecordingButtonMouseDown = false; |
| }); |
| |
| for (let recording of recordings) { |
| contextMenu.appendItem(recording.displayName, () => { |
| WI.showRepresentedObject(recording); |
| }); |
| } |
| |
| contextMenu.show(); |
| } |
| }; |