/* * 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(WI.Canvas.Event.RecordingStarted, this._updateRecordNavigationItem, this); this._canvas.removeEventListener(WI.Canvas.Event.RecordingStopped, this._updateRecordNavigationItem, this); this._canvas.recordingCollection.removeEventListener(WI.Collection.Event.ItemAdded, this._recordingAdded, this); this._canvas.recordingCollection.removeEventListener(WI.Collection.Event.ItemRemoved, this._recordingRemoved, 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.ready) { this._recording.removeEventListener(WI.Recording.Event.ProcessedAction, this._handleRecordingProcessedAction, this); this._recording.removeEventListener(WI.Recording.Event.StartProcessingFrame, this._handleRecordingStartProcessingFrame, 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; } attached() { super.attached(); 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); } } } detached() { this.contentBrowser.removeEventListener(WI.ContentBrowser.Event.CurrentRepresentedObjectsDidChange, this.updateRepresentedObjects, this); super.detached(); } canShowRepresentedObject(representedObject) { return representedObject instanceof WI.Canvas || representedObject instanceof WI.ShaderProgram || representedObject instanceof WI.Recording; } // 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); this.element.classList.toggle("showing-recording", !!this._recording); 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(WI.Recording.Event.ProcessedAction, this._handleRecordingProcessedAction, this); this._recording.removeEventListener(WI.Recording.Event.StartProcessingFrame, this._handleRecordingStartProcessingFrame, 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");