blob: c2bbdc02e9e896b42b1b63d334bcff22d8274a19 [file] [log] [blame]
/*
* Copyright (C) 2018 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.CanvasSidebarPanel = class CanvasSidebarPanel extends WI.NavigationSidebarPanel
{
constructor()
{
super("canvas", WI.UIString("Canvas"));
this._canvas = null;
this._recording = null;
this._navigationBar = new WI.NavigationBar;
this._scopeBar = null;
this._placeholderScopeBarItem = null;
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.enabled = false;
this._recordButtonNavigationItem.buttonStyle = WI.ButtonNavigationItem.Style.ImageAndText;
this._recordButtonNavigationItem.label = WI.UIString("Start");
this._recordButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._toggleRecording, this);
this._navigationBar.addNavigationItem(this._recordButtonNavigationItem);
this._navigationBar.addNavigationItem(new WI.DividerNavigationItem);
let importButtonNavigationItem = new WI.ButtonNavigationItem("import-recording", WI.UIString("Import"), "Images/Import.svg", 15, 15);
importButtonNavigationItem.buttonStyle = WI.ButtonNavigationItem.Style.ImageAndText;
importButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
importButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._handleImportButtonNavigationItemClicked, this);
this._navigationBar.addNavigationItem(importButtonNavigationItem);
this.addSubview(this._navigationBar);
this._canvasTreeOutline = this.createContentTreeOutline({suppressFiltering: true});
this._canvasTreeOutline.element.classList.add("canvas");
this._recordingNavigationBar = new WI.NavigationBar;
this._recordingNavigationBar.element.classList.add("hidden");
this.contentView.addSubview(this._recordingNavigationBar);
this._recordingContentContainer = this.contentView.element.appendChild(document.createElement("div"));
this._recordingContentContainer.className = "recording-content";
this._recordingTreeOutline = this.contentTreeOutline;
this._recordingContentContainer.appendChild(this._recordingTreeOutline.element);
this._recordingTreeOutline.customIndent = true;
this._recordingTreeOutline.registerScrollVirtualizer(this._recordingContentContainer, 20);
this._canvasTreeOutline.addEventListener(WI.TreeOutline.Event.SelectionDidChange, this._treeSelectionDidChange, this);
this._recordingTreeOutline.addEventListener(WI.TreeOutline.Event.SelectionDidChange, this._treeSelectionDidChange, this);
this._recordingProcessingOptionsContainer = null;
this._selectedRecordingActionIndex = NaN;
}
// Public
get canvas()
{
return this._canvas;
}
set canvas(canvas)
{
if (this._canvas === canvas)
return;
if (this._canvas) {
this._canvas.removeEventListener(null, null, this);
this._canvas.recordingCollection.removeEventListener(null, null, this);
}
this._canvas = canvas;
if (this._canvas) {
this._canvas.addEventListener(WI.Canvas.Event.RecordingStarted, this._updateRecordNavigationItem, this);
this._canvas.addEventListener(WI.Canvas.Event.RecordingStopped, this._updateRecordNavigationItem, this);
this._canvas.recordingCollection.addEventListener(WI.Collection.Event.ItemAdded, this._recordingAdded, this);
this._canvas.recordingCollection.addEventListener(WI.Collection.Event.ItemRemoved, this._recordingRemoved, this);
}
this._canvasChanged();
this._updateRecordNavigationItem();
this._updateRecordingScopeBar();
}
set recording(recording)
{
if (recording === this._recording)
return;
if (this._recording)
this._recording.removeEventListener(null, null, this);
if (recording)
this.canvas = recording.source;
this._recording = recording;
if (this._recording && !this._recording.ready) {
this._recording.addEventListener(WI.Recording.Event.ProcessedAction, this._handleRecordingProcessedAction, this);
this._recording.addEventListener(WI.Recording.Event.StartProcessingFrame, this._handleRecordingStartProcessingFrame, this);
}
this._updateRecordNavigationItem();
this._updateRecordingScopeBar();
this._recordingChanged();
}
set action(action)
{
if (!this._recording)
return;
if (action === this._recording.actions[this._selectedRecordingActionIndex])
return;
let selectedTreeElement = this._recordingTreeOutline.selectedTreeElement;
if (!action) {
if (selectedTreeElement)
selectedTreeElement.deselect();
return;
}
if (selectedTreeElement && selectedTreeElement instanceof WI.FolderTreeElement) {
let lastActionTreeElement = selectedTreeElement.children.lastValue;
if (action === lastActionTreeElement.representedObject)
return;
}
let treeElement = this._recordingTreeOutline.findTreeElement(action);
console.assert(treeElement, "Missing tree element for recording action.", action);
if (!treeElement)
return;
this._recording[WI.CanvasSidebarPanel.SelectedActionSymbol] = action;
const omitFocus = false;
const selectedByUser = false;
treeElement.revealAndSelect(omitFocus, selectedByUser);
this._selectedRecordingActionIndex = this._recording.actions.indexOf(action);
}
updateRepresentedObjects()
{
let objects = this.contentBrowser.currentRepresentedObjects;
let canvas = objects.find((object) => object instanceof WI.Canvas);
if (canvas) {
this.canvas = canvas;
let treeElement = this._canvasTreeOutline.findTreeElement(canvas);
const omitFocus = false;
const selectedByUser = false;
treeElement.revealAndSelect(omitFocus, selectedByUser);
return;
}
let shaderProgram = objects.find((object) => object instanceof WI.ShaderProgram);
if (shaderProgram) {
this.canvas = shaderProgram.canvas;
let treeElement = this._canvasTreeOutline.findTreeElement(shaderProgram);
const omitFocus = false;
const selectedByUser = false;
treeElement.revealAndSelect(omitFocus, selectedByUser);
return;
}
let recording = objects.find((object) => object instanceof WI.Recording);
if (recording) {
this.canvas = recording.source;
this.recording = recording;
let recordingAction = objects.find((object) => object instanceof WI.RecordingAction);
if (recordingAction !== recording[WI.CanvasSidebarPanel.SelectedActionSymbol])
this.action = recordingAction;
return;
}
this.canvas = null;
this.recording = null;
}
shown()
{
super.shown();
this.contentBrowser.addEventListener(WI.ContentBrowser.Event.CurrentRepresentedObjectsDidChange, this.updateRepresentedObjects, this);
this.updateRepresentedObjects();
if (this._recording) {
this._recordingTreeOutline.updateVirtualizedElementsDebouncer.force();
let action = this._recording[WI.CanvasSidebarPanel.SelectedActionSymbol];
let treeElement = this._recordingTreeOutline.findTreeElement(action);
if (treeElement) {
const omitFocus = false;
const selectedByUser = false;
treeElement.revealAndSelect(omitFocus, selectedByUser);
}
}
}
hidden()
{
this.contentBrowser.removeEventListener(null, null, this);
super.hidden();
}
canShowRepresentedObject(representedObject)
{
if (representedObject instanceof WI.CanvasCollection)
return false;
return super.canShowRepresentedObject(representedObject);
}
// Protected
get scrollElement()
{
return this._recordingContentContainer;
}
hasCustomFilters()
{
return true;
}
matchTreeElementAgainstCustomFilters(treeElement)
{
// Keep recording frame tree elements.
if (treeElement instanceof WI.FolderTreeElement)
return true;
// Always show the Initial State tree element.
if (treeElement instanceof WI.RecordingActionTreeElement && treeElement.representedObject instanceof WI.RecordingInitialStateAction)
return true;
return super.matchTreeElementAgainstCustomFilters(treeElement);
}
initialLayout()
{
super.initialLayout();
let filterFunction = (treeElement) => {
if (!(treeElement.representedObject instanceof WI.RecordingAction))
return false;
return treeElement.representedObject.isVisual || treeElement.representedObject instanceof WI.RecordingInitialStateAction;
};
const activatedByDefault = false;
const defaultToolTip = WI.UIString("Only show visual actions");
const activatedToolTip = WI.UIString("Show all actions");
this.filterBar.addFilterBarButton("recording-show-visual-only", filterFunction, activatedByDefault, defaultToolTip, activatedToolTip, "Images/Paint.svg", 15, 15);
}
// Private
_recordingAdded(event)
{
this.recording = event.data.item;
}
_recordingRemoved(event)
{
this._updateRecordingScopeBar();
let recording = event.data.item;
if (recording === this.recording)
this.recording = this._canvas ? Array.from(this._canvas.recordingCollection).lastValue : null;
}
_scopeBarSelectionChanged()
{
let selectedScopeBarItem = this._scopeBar.selectedItems[0];
this.recording = selectedScopeBarItem.__recording || null;
}
_toggleRecording(event)
{
if (!this._canvas)
return;
if (this._canvas.recordingActive)
this._canvas.stopRecording();
else {
let singleFrame = event.data.nativeEvent.shiftKey;
this._canvas.startRecording(singleFrame);
}
}
_handleImportButtonNavigationItemClicked(event)
{
WI.FileUtilities.importJSON((result) => WI.canvasManager.processJSON(result), {multiple: true});
}
_treeSelectionDidChange(event)
{
let treeElement = event.target.selectedTreeElement;
if (!treeElement)
return;
if ((treeElement instanceof WI.CanvasTreeElement) || (treeElement instanceof WI.ShaderProgramTreeElement)) {
if (this._placeholderScopeBarItem)
this._placeholderScopeBarItem.selected = true;
this.showDefaultContentViewForTreeElement(treeElement);
return;
}
if (treeElement instanceof WI.FolderTreeElement)
treeElement = treeElement.children.lastValue;
if (!(treeElement instanceof WI.RecordingActionTreeElement))
return;
console.assert(this._recording, "Missing recording for action tree element.", treeElement);
this._recording[WI.CanvasSidebarPanel.SelectedActionSymbol] = treeElement.representedObject;
const onlyExisting = true;
let recordingContentView = this.contentBrowser.contentViewForRepresentedObject(this._recording, onlyExisting);
if (!recordingContentView)
return;
this.contentBrowser.showContentView(recordingContentView);
this._selectedRecordingActionIndex = treeElement.index;
recordingContentView.updateActionIndex(this._selectedRecordingActionIndex);
}
_canvasChanged()
{
this._canvasTreeOutline.removeChildren();
if (!this._canvas) {
this._recordingNavigationBar.element.classList.add("hidden");
return;
}
const showRecordings = false;
let canvasTreeElement = new WI.CanvasTreeElement(this._canvas, showRecordings);
canvasTreeElement.expanded = true;
this._canvasTreeOutline.appendChild(canvasTreeElement);
const omitFocus = false;
const selectedByUser = false;
canvasTreeElement.revealAndSelect(omitFocus, selectedByUser);
if (WI.Canvas.ContextType.Canvas2D || this._canvas.contextType === WI.Canvas.ContextType.WebGL || this._canvas.contextType === WI.Canvas.ContextType.WebGL2)
this._recordButtonNavigationItem.enabled = true;
this.recording = null;
}
_recordingChanged()
{
this._recordingTreeOutline.removeChildren();
this._selectedRecordingActionIndex = NaN;
if (this._recordingProcessingOptionsContainer) {
this._recordingProcessingOptionsContainer.remove();
this._recordingProcessingOptionsContainer = null;
}
if (!this._recording)
return;
if (!this._recording.ready) {
if (!this._recording.processing)
this._recording.startProcessing();
if (!this._recordingProcessingOptionsContainer) {
this._recordingProcessingOptionsContainer = this._recordingContentContainer.appendChild(document.createElement("div"));
this._recordingProcessingOptionsContainer.classList.add("recording-processing-options");
let createPauseButton = () => {
let spinner = new WI.IndeterminateProgressSpinner;
this._recordingProcessingOptionsContainer.appendChild(spinner.element);
let pauseButton = this._recordingProcessingOptionsContainer.appendChild(document.createElement("button"));
pauseButton.textContent = WI.UIString("Pause Processing");
pauseButton.addEventListener("click", (event) => {
this._recording.stopProcessing();
spinner.element.remove();
pauseButton.remove();
createResumeButton();
});
};
let createResumeButton = () => {
let resumeButton = this._recordingProcessingOptionsContainer.appendChild(document.createElement("button"));
resumeButton.textContent = WI.UIString("Resume Processing");
resumeButton.addEventListener("click", (event) => {
this._recording.startProcessing();
resumeButton.remove();
createPauseButton();
});
};
if (this._recording.processing)
createPauseButton();
else
createResumeButton();
}
}
this.contentBrowser.showContentViewForRepresentedObject(this._recording);
if (this._scopeBar) {
let scopeBarItem = this._scopeBar.item(this._recording.displayName);
console.assert(scopeBarItem, "Missing scopeBarItem for recording.", this._recording);
scopeBarItem.selected = true;
}
let initialStateAction = this._recording.actions[0];
if (initialStateAction.ready && !this._recordingTreeOutline.getCachedTreeElement(initialStateAction)) {
this._recordingTreeOutline.appendChild(new WI.RecordingActionTreeElement(initialStateAction, 0, this._recording.type));
if (!this._recording[WI.CanvasSidebarPanel.SelectedActionSymbol])
this.action = initialStateAction;
}
let cumulativeActionIndex = 0;
this._recording.frames.forEach((frame, frameIndex) => {
if (!frame.actions[0].ready)
return;
let folder = this._recordingTreeOutline.getCachedTreeElement(frame);
if (!folder)
folder = this._createRecordingFrameTreeElement(frame, frameIndex, this._recordingTreeOutline);
for (let action of frame.actions) {
++cumulativeActionIndex;
if (!action.ready || this._recordingTreeOutline.getCachedTreeElement(action))
break;
this._createRecordingActionTreeElement(action, cumulativeActionIndex, folder);
}
});
}
_updateRecordNavigationItem()
{
if (!this._canvas || !(this._canvas.contextType === WI.Canvas.ContextType.Canvas2D || this._canvas.contextType === WI.Canvas.ContextType.WebGL || this._canvas.contextType === WI.Canvas.ContextType.WebGL2)) {
this._recordButtonNavigationItem.enabled = false;
return;
}
this._recordButtonNavigationItem.toggled = this._canvas.recordingActive;
this._recordButtonNavigationItem.label = this._recordButtonNavigationItem.toggled ? WI.UIString("Stop") : WI.UIString("Start");
}
_updateRecordingScopeBar()
{
if (this._scopeBar) {
this._placeholderScopeBarItem = null;
this._recordingNavigationBar.removeNavigationItem(this._scopeBar);
this._scopeBar = null;
}
this._recordingNavigationBar.element.classList.toggle("hidden", !this._canvas);
let hasRecordings = this._recording || (this._canvas && this._canvas.recordingCollection.size);
this.element.classList.toggle("has-recordings", hasRecordings);
if (!hasRecordings)
return;
let scopeBarItems = [];
let selectedScopeBarItem = null;
let createScopeBarItem = (recording) => {
let scopeBarItem = new WI.ScopeBarItem(recording.displayName, recording.displayName);
if (recording === this._recording)
selectedScopeBarItem = scopeBarItem;
else
scopeBarItem.selected = false;
scopeBarItem.__recording = recording;
scopeBarItems.push(scopeBarItem);
};
if (this._canvas && this._canvas.recordingCollection) {
for (let recording of this._canvas.recordingCollection)
createScopeBarItem(recording);
}
if (this._recording && (!this._canvas || !this._canvas.recordingCollection.has(this._recording)))
createScopeBarItem(this._recording);
if (!selectedScopeBarItem) {
selectedScopeBarItem = scopeBarItems[0];
this._placeholderScopeBarItem = new WI.ScopeBarItem("canvas-recording-scope-bar-item-placeholder", WI.UIString("Recordings"), {exclusive: true, hidden: true});
this._placeholderScopeBarItem.selected = true;
scopeBarItems.unshift(this._placeholderScopeBarItem);
}
this._scopeBar = new WI.ScopeBar("canvas-recording-scope-bar", scopeBarItems, selectedScopeBarItem, true);
this._scopeBar.addEventListener(WI.ScopeBar.Event.SelectionChanged, this._scopeBarSelectionChanged, this);
this._recordingNavigationBar.insertNavigationItem(this._scopeBar, 0);
}
_createRecordingFrameTreeElement(frame, index, parent)
{
let folder = new WI.FolderTreeElement(WI.UIString("Frame %d").format((index + 1).toLocaleString()), frame);
if (!isNaN(frame.duration)) {
const higherResolution = true;
folder.status = Number.secondsToString(frame.duration / 1000, higherResolution);
}
parent.appendChild(folder);
return folder;
}
_createRecordingActionTreeElement(action, index, parent)
{
let treeElement = new WI.RecordingActionTreeElement(action, index, this._recording.type);
parent.appendChild(treeElement);
if (parent instanceof WI.FolderTreeElement && parent.representedObject instanceof WI.RecordingFrame) {
if (action !== parent.representedObject.actions.lastValue) {
parent.addClassName("processing");
if (!(parent.subtitle instanceof HTMLProgressElement))
parent.subtitle = document.createElement("progress");
if (parent.statusElement)
parent.subtitle.style.setProperty("width", `calc(100% - ${parent.statusElement.offsetWidth + 4}px`);
parent.subtitle.value = parent.representedObject.actions.indexOf(action) / parent.representedObject.actions.length;
} else {
parent.removeClassName("processing");
if (parent.representedObject.incomplete)
parent.subtitle = WI.UIString("Incomplete");
else
parent.subtitle = "";
}
}
if (action === this._recording[WI.CanvasSidebarPanel.SelectedActionSymbol])
this.action = action;
return treeElement;
}
_handleRecordingProcessedAction(event)
{
let {action, index} = event.data;
this._recordingTreeOutline.element.dataset.indent = Number.countDigits(index);
let isInitialStateAction = !index;
console.assert(isInitialStateAction || this._recordingTreeOutline.children.lastValue instanceof WI.FolderTreeElement, "There should be a WI.FolderTreeElement for the frame for this action.");
this._createRecordingActionTreeElement(action, index, isInitialStateAction ? this._recordingTreeOutline : this._recordingTreeOutline.children.lastValue);
if (!this._recording[WI.CanvasSidebarPanel.SelectedActionSymbol]) {
console.assert(action === this._recording.actions[0]);
this.action = this._recording.actions[0];
}
if (this._recording.ready) {
this._recording.removeEventListener(null, null, this);
if (this._recordingProcessingOptionsContainer) {
this._recordingProcessingOptionsContainer.remove();
this._recordingProcessingOptionsContainer = null;
}
}
}
_handleRecordingStartProcessingFrame(event)
{
let {frame, index} = event.data;
this._createRecordingFrameTreeElement(frame, index, this._recordingTreeOutline);
}
};
WI.CanvasSidebarPanel.SelectedActionSymbol = Symbol("selected-action");