blob: 80688401231b85a4ad81f5a21e40c97124d36628 [file] [log] [blame]
/*
* 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();
}
};