1/*
2 * Copyright (C) 2018 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26WI.CanvasSidebarPanel = class CanvasSidebarPanel extends WI.NavigationSidebarPanel
27{
28 constructor()
29 {
30 super("canvas", WI.UIString("Canvas"));
31
32 this._canvas = null;
33 this._recording = null;
34
35 this._navigationBar = new WI.NavigationBar;
36 this._scopeBar = null;
37
38 const toolTip = WI.UIString("Start recording canvas actions.\nShift-click to record a single frame.");
39 const altToolTip = WI.UIString("Stop recording canvas actions");
40 this._recordButtonNavigationItem = new WI.ToggleButtonNavigationItem("record-start-stop", toolTip, altToolTip, "Images/Record.svg", "Images/Stop.svg", 13, 13);
41 this._recordButtonNavigationItem.enabled = false;
42 this._recordButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._toggleRecording, this);
43 this._navigationBar.addNavigationItem(this._recordButtonNavigationItem);
44
45 this.addSubview(this._navigationBar);
46
47 const suppressFiltering = true;
48 this._canvasTreeOutline = this.createContentTreeOutline(suppressFiltering);
49 this._canvasTreeOutline.element.classList.add("canvas");
50
51 this._recordingNavigationBar = new WI.NavigationBar;
52 this.contentView.addSubview(this._recordingNavigationBar);
53
54 this._recordingTreeOutline = this.contentTreeOutline;
55 this.contentView.element.appendChild(this._recordingTreeOutline.element);
56
57 this._canvasTreeOutline.addEventListener(WI.TreeOutline.Event.SelectionDidChange, this._treeOutlineSelectionDidChange, this);
58 this._recordingTreeOutline.addEventListener(WI.TreeOutline.Event.SelectionDidChange, this._treeOutlineSelectionDidChange, this);
59
60 WI.canvasManager.addEventListener(WI.CanvasManager.Event.RecordingStarted, this._updateRecordNavigationItem, this);
61 WI.canvasManager.addEventListener(WI.CanvasManager.Event.RecordingStopped, this._updateRecordNavigationItem, this);
62 }
63
64 // Public
65
66 get canvas()
67 {
68 return this._canvas;
69 }
70
71 set canvas(canvas)
72 {
73 if (this._canvas === canvas)
74 return;
75
76 if (this._canvas)
77 this._canvas.recordingCollection.removeEventListener(null, null, this);
78
79 this._canvas = canvas;
80 if (this._canvas) {
81 this._canvas.recordingCollection.addEventListener(WI.Collection.Event.ItemAdded, this._recordingAdded, this);
82 this._canvas.recordingCollection.addEventListener(WI.Collection.Event.ItemRemoved, this._recordingRemoved, this);
83 }
84
85 this._canvasChanged();
86 this._updateRecordNavigationItem();
87 this._updateRecordingScopeBar();
88 }
89
90 set recording(recording)
91 {
92 if (recording === this._recording)
93 return;
94
95 if (recording)
96 this.canvas = recording.source;
97
98 this._recording = recording;
99 this._recordingChanged();
100 }
101
102 set action(action)
103 {
104 if (!this._recording)
105 return;
106
107 let treeElement = this._recordingTreeOutline.findTreeElement(action);
108 console.assert(treeElement, "Missing tree element for recording action.", action);
109 if (!treeElement)
110 return;
111
112 this._recording[WI.CanvasSidebarPanel.SelectedActionSymbol] = action;
113
114 const omitFocus = false;
115 const selectedByUser = false;
116 treeElement.revealAndSelect(omitFocus, selectedByUser);
117 }
118
119 shown()
120 {
121 super.shown();
122
123 this.contentBrowser.addEventListener(WI.ContentBrowser.Event.CurrentRepresentedObjectsDidChange, this._currentRepresentedObjectsDidChange, this);
124 }
125
126 hidden()
127 {
128 this.contentBrowser.removeEventListener(null, null, this);
129
130 super.hidden();
131 }
132
133 // Protected
134
135 hasCustomFilters()
136 {
137 return true;
138 }
139
140 matchTreeElementAgainstCustomFilters(treeElement)
141 {
142 // Keep recording frame tree elements.
143 if (treeElement instanceof WI.FolderTreeElement)
144 return true;
145
146 // Always show the Initial State tree element.
147 if (treeElement instanceof WI.RecordingActionTreeElement && treeElement.representedObject instanceof WI.RecordingInitialStateAction)
148 return true;
149
150 return super.matchTreeElementAgainstCustomFilters(treeElement);
151 }
152
153 initialLayout()
154 {
155 super.initialLayout();
156
157 let filterFunction = (treeElement) => {
158 if (!(treeElement.representedObject instanceof WI.RecordingAction))
159 return false;
160
161 return treeElement.representedObject.isVisual || treeElement.representedObject instanceof WI.RecordingInitialStateAction;
162 };
163
164 const activatedByDefault = false;
165 const defaultToolTip = WI.UIString("Only show visual actions");
166 const activatedToolTip = WI.UIString("Show all actions");
167 this.filterBar.addFilterBarButton("recording-show-visual-only", filterFunction, activatedByDefault, defaultToolTip, activatedToolTip, "Images/Paint.svg", 15, 15);
168 }
169
170 // Private
171
172 _recordingAdded(event)
173 {
174 this.recording = event.data.item;
175
176 this._updateRecordNavigationItem();
177 this._updateRecordingScopeBar();
178 }
179
180 _recordingRemoved(event)
181 {
182 let recording = event.data.item;
183 if (recording === this.recording)
184 this.recording = this._canvas ? this._canvas.recordingCollection.toArray().lastValue : null;
185
186 this._updateRecordingScopeBar();
187 }
188
189 _scopeBarSelectionChanged()
190 {
191 let selectedScopeBarItem = this._scopeBar.selectedItems[0];
192 this.recording = selectedScopeBarItem.__recording || null;
193 }
194
195 _toggleRecording(event)
196 {
197 if (!this._canvas)
198 return;
199
200 if (this._canvas.isRecording)
201 WI.canvasManager.stopRecording();
202 else if (!WI.canvasManager.recordingCanvas) {
203 let singleFrame = event.data.nativeEvent.shiftKey;
204 WI.canvasManager.startRecording(this._canvas, singleFrame);
205 }
206 }
207
208 _currentRepresentedObjectsDidChange(event)
209 {
210 let objects = this.contentBrowser.currentRepresentedObjects;
211
212 let canvas = objects.find((object) => object instanceof WI.Canvas);
213 if (canvas) {
214 this.canvas = canvas;
215 return;
216 }
217
218 let shaderProgram = objects.find((object) => object instanceof WI.ShaderProgram);
219 if (shaderProgram) {
220 this.canvas = shaderProgram.canvas;
221 let treeElement = this._canvasTreeOutline.findTreeElement(shaderProgram);
222 const omitFocus = false;
223 const selectedByUser = false;
224 treeElement.revealAndSelect(omitFocus, selectedByUser);
225 return;
226 }
227
228 let recording = objects.find((object) => object instanceof WI.Recording);
229 if (recording) {
230 this.canvas = recording.source;
231 this.recording = recording;
232 this.action = objects.find((object) => object instanceof WI.RecordingAction);
233 return;
234 }
235
236 this.canvas = null;
237 }
238
239 _treeOutlineSelectionDidChange(event)
240 {
241 let treeElement = event.data.selectedElement;
242 if (!treeElement)
243 return;
244
245 if ((treeElement instanceof WI.CanvasTreeElement) || (treeElement instanceof WI.ShaderProgramTreeElement)) {
246 this.showDefaultContentViewForTreeElement(treeElement);
247 return;
248 }
249
250 if (treeElement instanceof WI.FolderTreeElement)
251 treeElement = treeElement.children.lastValue;
252
253 if (!(treeElement instanceof WI.RecordingActionTreeElement))
254 return;
255
256 console.assert(this._recording, "Missing recording for action tree element.", treeElement);
257 this._recording[WI.CanvasSidebarPanel.SelectedActionSymbol] = treeElement.representedObject;
258
259 let recordingContentView = this.contentBrowser.showContentViewForRepresentedObject(this._recording);
260 if (recordingContentView)
261 recordingContentView.updateActionIndex(treeElement.index);
262 }
263
264 _canvasChanged()
265 {
266 this._canvasTreeOutline.removeChildren();
267
268 if (!this._canvas)
269 return;
270
271 const showRecordings = false;
272 let canvasTreeElement = new WI.CanvasTreeElement(this._canvas, showRecordings);
273 canvasTreeElement.expanded = true;
274 this._canvasTreeOutline.appendChild(canvasTreeElement);
275
276 const omitFocus = false;
277 const selectedByUser = false;
278 canvasTreeElement.revealAndSelect(omitFocus, selectedByUser);
279
280 if (WI.Canvas.ContextType.Canvas2D || this._canvas.contextType === WI.Canvas.ContextType.WebGL)
281 this._recordButtonNavigationItem.enabled = true;
282
283 let defaultSelectedRecording = null;
284 if (this._canvas.recordingCollection.items.size)
285 defaultSelectedRecording = this._canvas.recordingCollection.toArray().lastValue;
286
287 this.recording = defaultSelectedRecording;
288 }
289
290 _recordingChanged()
291 {
292 this._recordingTreeOutline.removeChildren();
293
294 if (!this._recording)
295 return;
296
297 let recording = this._recording;
298
299 this._recording.actions.then((actions) => {
300 if (recording !== this._recording)
301 return;
302
303 this._recordingTreeOutline.element.dataset.indent = Number.countDigits(actions.length);
304
305 if (actions[0] instanceof WI.RecordingInitialStateAction)
306 this._recordingTreeOutline.appendChild(new WI.RecordingActionTreeElement(actions[0], 0, this._recording.type));
307
308 let cumulativeActionIndex = 1;
309 this._recording.frames.forEach((frame, frameIndex) => {
310 let folder = new WI.FolderTreeElement(WI.UIString("Frame %d").format((frameIndex + 1).toLocaleString()));
311 this._recordingTreeOutline.appendChild(folder);
312
313 for (let i = 0; i < frame.actions.length; ++i)
314 folder.appendChild(new WI.RecordingActionTreeElement(frame.actions[i], cumulativeActionIndex + i, this._recording.type));
315
316 if (!isNaN(frame.duration)) {
317 const higherResolution = true;
318 folder.status = Number.secondsToString(frame.duration / 1000, higherResolution);
319 }
320
321 if (frame.incomplete)
322 folder.subtitle = WI.UIString("Incomplete");
323
324 if (this._recording.frames.length === 1)
325 folder.expand();
326
327 cumulativeActionIndex += frame.actions.length;
328 });
329
330 if (this._scopeBar) {
331 let scopeBarItem = this._scopeBar.item(this._recording.displayName);
332 console.assert(scopeBarItem, "Missing scopeBarItem for recording.", this._recording);
333 scopeBarItem.selected = true;
334 }
335
336 this.action = this._recording[WI.CanvasSidebarPanel.SelectedActionSymbol] || null;
337 });
338 }
339
340 _updateRecordNavigationItem()
341 {
342 if (!this._canvas || !(this._canvas.contextType === WI.Canvas.ContextType.Canvas2D || this._canvas.contextType === WI.Canvas.ContextType.WebGL)) {
343 this._recordButtonNavigationItem.enabled = false;
344 return;
345 }
346
347 let isRecording = this._canvas.isRecording;
348 this._recordButtonNavigationItem.enabled = isRecording || !WI.canvasManager.recordingCanvas;
349 this._recordButtonNavigationItem.toggled = isRecording;
350 }
351
352 _updateRecordingScopeBar()
353 {
354 if (this._scopeBar) {
355 this._recordingNavigationBar.removeNavigationItem(this._scopeBar);
356 this._scopeBar = null;
357 }
358
359 if (!this._canvas || !this._canvas.recordingCollection.items.size) {
360 this.element.classList.remove("has-recordings");
361 return;
362 }
363
364 let scopeBarItems = [];
365 let selectedScopeBarItem = null;
366 for (let recording of this._canvas.recordingCollection.items) {
367 let scopeBarItem = new WI.ScopeBarItem(recording.displayName, recording.displayName);
368 if (recording === this._recording)
369 selectedScopeBarItem = scopeBarItem;
370 scopeBarItem.__recording = recording;
371 scopeBarItems.push(scopeBarItem);
372 }
373
374 if (!selectedScopeBarItem)
375 selectedScopeBarItem = scopeBarItems[0];
376
377 this._scopeBar = new WI.ScopeBar("canvas-recordinga-scope-bar", scopeBarItems, selectedScopeBarItem, true);
378 this._scopeBar.addEventListener(WI.ScopeBar.Event.SelectionChanged, this._scopeBarSelectionChanged, this);
379 this._recordingNavigationBar.insertNavigationItem(this._scopeBar, 0);
380
381 this.element.classList.add("has-recordings");
382 }
383};
384
385WI.CanvasSidebarPanel.SelectedActionSymbol = Symbol("selected-action");