/* * 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.handleRefreshButtonClicked, this); this._showGridButtonNavigationItem = new WI.ActivateButtonNavigationItem("show-grid", WI.repeatedUIString.showTransparencyGridTooltip(), WI.UIString("Hide transparency 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(); }); } handleRefreshButtonClicked() { this.refreshPreview(); } // 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); if (this.representedObject.contextAttributes.colorSpace) { let subtitle = titles.appendChild(document.createElement("span")); subtitle.className = "color-space"; subtitle.textContent = "(" + WI.Canvas.displayNameForColorSpace(this.representedObject.contextAttributes.colorSpace) + ")"; } 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; WI.addMouseDownContextMenuHandlers(canvasElementButtonNavigationItem.element, this._populateCanvasElementButtonContextMenu.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"); WI.addMouseDownContextMenuHandlers(this._viewShaderButton, this._populateViewShaderButtonContextMenu.bind(this)); this._viewRecordingButton = document.createElement("img"); this._viewRecordingButton.classList.add("view-recording"); this._viewRecordingButton.title = WI.UIString("View Recording"); WI.addMouseDownContextMenuHandlers(this._viewRecordingButton, this._populateViewRecordingButtonContextMenu.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(); } 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) => { if (!node) return; 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); this.refreshPreview(); } detached() { this.representedObject.removeEventListener(WI.Canvas.Event.MemoryChanged, this._updateMemoryCost, this); this.representedObject.removeEventListener(WI.Canvas.Event.RecordingStarted, this.needsLayout, this); this.representedObject.removeEventListener(WI.Canvas.Event.RecordingProgress, this.needsLayout, this); this.representedObject.removeEventListener(WI.Canvas.Event.RecordingStopped, this.needsLayout, this); this.representedObject.shaderProgramCollection.removeEventListener(WI.Collection.Event.ItemAdded, this.needsLayout, this); this.representedObject.shaderProgramCollection.removeEventListener(WI.Collection.Event.ItemRemoved, this.needsLayout, this); if (this._canvasNode) { this._canvasNode.removeEventListener(WI.DOMNode.Event.AttributeModified, this._refreshPixelSize, this); this._canvasNode.removeEventListener(WI.DOMNode.Event.AttributeRemoved, this._refreshPixelSize, this); this._canvasNode = null; } WI.settings.showImageGrid.removeEventListener(WI.Setting.Event.Changed, this._updateImageGrid, this); super.detached(); } // Private _showError() { if (this._previewImageElement) this._previewImageElement.remove(); if (!this._errorElement) { let isError = WI.Canvas.supportsRequestContentForContextType(this.representedObject.contextType); 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); }); } _populateCanvasElementButtonContextMenu(contextMenu) { 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"); WI.consoleLogViewController.appendImmediateExecutionWithResult(text, remoteObject, {addSpecialUserLogClass: true}); }); }); contextMenu.appendSeparator(); if (this._canvasNode) WI.appendContextMenuItemsForDOMNode(contextMenu, this._canvasNode); } _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); } _populateViewShaderButtonContextMenu(contextMenu, event) { let shaderPrograms = this.representedObject.shaderProgramCollection; console.assert(shaderPrograms.size); if (!shaderPrograms.size) return; if (event.button === 0 && shaderPrograms.size === 1) { WI.showRepresentedObject(Array.from(shaderPrograms)[0]); return; } for (let shaderProgram of shaderPrograms) { contextMenu.appendItem(shaderProgram.displayName, () => { WI.showRepresentedObject(shaderProgram); }); } } _populateViewRecordingButtonContextMenu(contextMenu, event) { let recordings = this.representedObject.recordingCollection; console.assert(recordings.size); if (!recordings.size) return; if (event.button === 0 && recordings.size === 1) { WI.showRepresentedObject(Array.from(recordings)[0]); return; } for (let recording of recordings) { contextMenu.appendItem(recording.displayName, () => { WI.showRepresentedObject(recording); }); } } };