75c30b5320eec0e3d47e7a03f9c427e1dcc143ea
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / CanvasSidebarPanel.js
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
26 WI.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         this._placeholderScopeBarItem = null;
38
39         const toolTip = WI.UIString("Start recording canvas actions.\nShift-click to record a single frame.");
40         const altToolTip = WI.UIString("Stop recording canvas actions");
41         this._recordButtonNavigationItem = new WI.ToggleButtonNavigationItem("record-start-stop", toolTip, altToolTip, "Images/Record.svg", "Images/Stop.svg", 13, 13);
42         this._recordButtonNavigationItem.enabled = false;
43         this._recordButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._toggleRecording, this);
44         this._navigationBar.addNavigationItem(this._recordButtonNavigationItem);
45
46         this.addSubview(this._navigationBar);
47
48         const suppressFiltering = true;
49         this._canvasTreeOutline = this.createContentTreeOutline(suppressFiltering);
50         this._canvasTreeOutline.element.classList.add("canvas");
51
52         this._recordingNavigationBar = new WI.NavigationBar;
53         this._recordingNavigationBar.element.classList.add("hidden");
54         this.contentView.addSubview(this._recordingNavigationBar);
55
56         this._recordingContentContainer = this.contentView.element.appendChild(document.createElement("div"));
57         this._recordingContentContainer.className = "recording-content";
58
59         this._recordingTreeOutline = this.contentTreeOutline;
60         this._recordingContentContainer.appendChild(this._recordingTreeOutline.element);
61
62         this._recordingTreeOutline.customIndent = true;
63         this._recordingTreeOutline.registerScrollVirtualizer(this._recordingContentContainer, 20);
64
65         this._canvasTreeOutline.addEventListener(WI.TreeOutline.Event.SelectionDidChange, this._treeOutlineSelectionDidChange, this);
66         this._recordingTreeOutline.addEventListener(WI.TreeOutline.Event.SelectionDidChange, this._treeOutlineSelectionDidChange, this);
67
68         WI.canvasManager.addEventListener(WI.CanvasManager.Event.RecordingStarted, this._updateRecordNavigationItem, this);
69         WI.canvasManager.addEventListener(WI.CanvasManager.Event.RecordingStopped, this._updateRecordNavigationItem, this);
70
71         this._recordingProcessPromise = null;
72         this._recordingProcessSpinner = null;
73     }
74
75     // Public
76
77     get canvas()
78     {
79         return this._canvas;
80     }
81
82     set canvas(canvas)
83     {
84         if (this._canvas === canvas)
85             return;
86
87         if (this._canvas)
88             this._canvas.recordingCollection.removeEventListener(null, null, this);
89
90         this._canvas = canvas;
91         if (this._canvas) {
92             this._canvas.recordingCollection.addEventListener(WI.Collection.Event.ItemAdded, this._recordingAdded, this);
93             this._canvas.recordingCollection.addEventListener(WI.Collection.Event.ItemRemoved, this._recordingRemoved, this);
94         }
95
96         this._canvasChanged();
97         this._updateRecordNavigationItem();
98         this._updateRecordingScopeBar();
99     }
100
101     set recording(recording)
102     {
103         if (recording === this._recording)
104             return;
105
106         if (recording)
107             this.canvas = recording.source;
108
109         this._recording = recording;
110         this._recordingChanged();
111     }
112
113     set action(action)
114     {
115         if (!this._recording || this._recordingProcessPromise)
116             return;
117
118         let selectedTreeElement = this._recordingTreeOutline.selectedTreeElement;
119         if (!action) {
120             if (selectedTreeElement)
121                 selectedTreeElement.deselect();
122             return;
123         }
124
125         if (selectedTreeElement && selectedTreeElement instanceof WI.FolderTreeElement) {
126             let lastActionTreeElement = selectedTreeElement.children.lastValue;
127             if (action === lastActionTreeElement.representedObject)
128                 return;
129         }
130
131         let treeElement = this._recordingTreeOutline.findTreeElement(action);
132         console.assert(treeElement, "Missing tree element for recording action.", action);
133         if (!treeElement)
134             return;
135
136         this._recording[WI.CanvasSidebarPanel.SelectedActionSymbol] = action;
137
138         const omitFocus = false;
139         const selectedByUser = false;
140         treeElement.revealAndSelect(omitFocus, selectedByUser);
141     }
142
143     shown()
144     {
145         super.shown();
146
147         this.contentBrowser.addEventListener(WI.ContentBrowser.Event.CurrentRepresentedObjectsDidChange, this._currentRepresentedObjectsDidChange, this);
148         this._currentRepresentedObjectsDidChange();
149     }
150
151     hidden()
152     {
153         this.contentBrowser.removeEventListener(null, null, this);
154
155         super.hidden();
156     }
157
158     canShowRepresentedObject(representedObject)
159     {
160         if (representedObject instanceof WI.CanvasCollection)
161             return false;
162
163         return super.canShowRepresentedObject(representedObject);
164     }
165
166     // Protected
167
168     hasCustomFilters()
169     {
170         return true;
171     }
172
173     matchTreeElementAgainstCustomFilters(treeElement)
174     {
175         // Keep recording frame tree elements.
176         if (treeElement instanceof WI.FolderTreeElement)
177             return true;
178
179         // Always show the Initial State tree element.
180         if (treeElement instanceof WI.RecordingActionTreeElement && treeElement.representedObject instanceof WI.RecordingInitialStateAction)
181             return true;
182
183         return super.matchTreeElementAgainstCustomFilters(treeElement);
184     }
185
186     initialLayout()
187     {
188         super.initialLayout();
189
190         let filterFunction = (treeElement) => {
191             if (!(treeElement.representedObject instanceof WI.RecordingAction))
192                 return false;
193
194             return treeElement.representedObject.isVisual || treeElement.representedObject instanceof WI.RecordingInitialStateAction;
195         };
196
197         const activatedByDefault = false;
198         const defaultToolTip = WI.UIString("Only show visual actions");
199         const activatedToolTip = WI.UIString("Show all actions");
200         this.filterBar.addFilterBarButton("recording-show-visual-only", filterFunction, activatedByDefault, defaultToolTip, activatedToolTip, "Images/Paint.svg", 15, 15);
201     }
202
203     // Private
204
205     _recordingAdded(event)
206     {
207         this.recording = event.data.item;
208
209         this._updateRecordNavigationItem();
210         this._updateRecordingScopeBar();
211     }
212
213     _recordingRemoved(event)
214     {
215         let recording = event.data.item;
216         if (recording === this.recording)
217             this.recording = this._canvas ? Array.from(this._canvas.recordingCollection).lastValue : null;
218
219         this._updateRecordingScopeBar();
220     }
221
222     _scopeBarSelectionChanged()
223     {
224         let selectedScopeBarItem = this._scopeBar.selectedItems[0];
225         this.recording = selectedScopeBarItem.__recording || null;
226     }
227
228     _toggleRecording(event)
229     {
230         if (!this._canvas)
231             return;
232
233         if (this._canvas.isRecording)
234             WI.canvasManager.stopRecording();
235         else if (!WI.canvasManager.recordingCanvas) {
236             let singleFrame = event.data.nativeEvent.shiftKey;
237             WI.canvasManager.startRecording(this._canvas, singleFrame);
238         }
239     }
240
241     _currentRepresentedObjectsDidChange(event)
242     {
243         let objects = this.contentBrowser.currentRepresentedObjects;
244
245         let canvas = objects.find((object) => object instanceof WI.Canvas);
246         if (canvas) {
247             this.canvas = canvas;
248             return;
249         }
250
251         let shaderProgram = objects.find((object) => object instanceof WI.ShaderProgram);
252         if (shaderProgram) {
253             this.canvas = shaderProgram.canvas;
254             let treeElement = this._canvasTreeOutline.findTreeElement(shaderProgram);
255             const omitFocus = false;
256             const selectedByUser = false;
257             treeElement.revealAndSelect(omitFocus, selectedByUser);
258             return;
259         }
260
261         let recording = objects.find((object) => object instanceof WI.Recording);
262         if (recording) {
263             recording[WI.CanvasSidebarPanel.SelectedActionSymbol] = objects.find((object) => object instanceof WI.RecordingAction);
264             this.recording = recording;
265             return;
266         }
267
268         this.canvas = null;
269         this.recording = null;
270     }
271
272     _treeOutlineSelectionDidChange(event)
273     {
274         let treeElement = event.data.selectedElement;
275         if (!treeElement)
276             return;
277
278         if ((treeElement instanceof WI.CanvasTreeElement) || (treeElement instanceof WI.ShaderProgramTreeElement)) {
279             if (this._placeholderScopeBarItem)
280                 this._placeholderScopeBarItem.selected = true;
281
282             this.showDefaultContentViewForTreeElement(treeElement);
283             return;
284         }
285
286         if (treeElement instanceof WI.FolderTreeElement)
287             treeElement = treeElement.children.lastValue;
288
289         if (!(treeElement instanceof WI.RecordingActionTreeElement))
290             return;
291
292         console.assert(this._recording, "Missing recording for action tree element.", treeElement);
293         this._recording[WI.CanvasSidebarPanel.SelectedActionSymbol] = treeElement.representedObject;
294
295         const onlyExisting = true;
296         let recordingContentView = this.contentBrowser.contentViewForRepresentedObject(this._recording, onlyExisting);
297         if (recordingContentView)
298             recordingContentView.updateActionIndex(treeElement.index);
299     }
300
301     _canvasChanged()
302     {
303         this._canvasTreeOutline.removeChildren();
304
305         if (!this._canvas) {
306             this._recordingNavigationBar.element.classList.add("hidden");
307             return;
308         }
309
310         const showRecordings = false;
311         let canvasTreeElement = new WI.CanvasTreeElement(this._canvas, showRecordings);
312         canvasTreeElement.expanded = true;
313         this._canvasTreeOutline.appendChild(canvasTreeElement);
314
315         const omitFocus = false;
316         const selectedByUser = false;
317         canvasTreeElement.revealAndSelect(omitFocus, selectedByUser);
318
319         if (WI.Canvas.ContextType.Canvas2D || this._canvas.contextType === WI.Canvas.ContextType.WebGL)
320             this._recordButtonNavigationItem.enabled = true;
321
322         this.recording = null;
323     }
324
325     _recordingChanged()
326     {
327         this._recordingTreeOutline.removeChildren();
328
329         if (!this._recording)
330             return;
331
332         if (!this._recordingProcessSpinner) {
333             this._recordingProcessSpinner = new WI.IndeterminateProgressSpinner;
334             this._recordingContentContainer.appendChild(this._recordingProcessSpinner.element);
335         }
336
337         this.contentBrowser.showContentViewForRepresentedObject(this._recording);
338
339         let recording = this._recording;
340
341         let promise = this._recording.process().then(() => {
342             if (recording !== this._recording || promise !== this._recordingProcessPromise)
343                 return;
344
345             if (this._recordingProcessSpinner) {
346                 this._recordingProcessSpinner.element.remove();
347                 this._recordingProcessSpinner = null;
348             }
349
350             this._recordingTreeOutline.element.dataset.indent = Number.countDigits(this._recording.actions.length);
351
352             if (this._recording.actions[0] instanceof WI.RecordingInitialStateAction)
353                 this._recordingTreeOutline.appendChild(new WI.RecordingActionTreeElement(this._recording.actions[0], 0, this._recording.type));
354
355             let cumulativeActionIndex = 1;
356             this._recording.frames.forEach((frame, frameIndex) => {
357                 let folder = new WI.FolderTreeElement(WI.UIString("Frame %d").format((frameIndex + 1).toLocaleString()));
358                 this._recordingTreeOutline.appendChild(folder);
359
360                 for (let i = 0; i < frame.actions.length; ++i)
361                     folder.appendChild(new WI.RecordingActionTreeElement(frame.actions[i], cumulativeActionIndex + i, this._recording.type));
362
363                 if (!isNaN(frame.duration)) {
364                     const higherResolution = true;
365                     folder.status = Number.secondsToString(frame.duration / 1000, higherResolution);
366                 }
367
368                 if (frame.incomplete)
369                     folder.subtitle = WI.UIString("Incomplete");
370
371                 if (this._recording.frames.length === 1)
372                     folder.expand();
373
374                 cumulativeActionIndex += frame.actions.length;
375             });
376
377             if (this._scopeBar) {
378                 let scopeBarItem = this._scopeBar.item(this._recording.displayName);
379                 console.assert(scopeBarItem, "Missing scopeBarItem for recording.", this._recording);
380                 scopeBarItem.selected = true;
381             }
382
383             this.action = this._recording[WI.CanvasSidebarPanel.SelectedActionSymbol] || this._recording.actions[0];
384
385             this._recordingProcessPromise = null;
386         });
387
388         this._recordingProcessPromise = promise;
389     }
390
391     _updateRecordNavigationItem()
392     {
393         if (!this._canvas || !(this._canvas.contextType === WI.Canvas.ContextType.Canvas2D || this._canvas.contextType === WI.Canvas.ContextType.WebGL)) {
394             this._recordButtonNavigationItem.enabled = false;
395             return;
396         }
397
398         let isRecording = this._canvas.isRecording;
399         this._recordButtonNavigationItem.enabled = isRecording || !WI.canvasManager.recordingCanvas;
400         this._recordButtonNavigationItem.toggled = isRecording;
401     }
402
403     _updateRecordingScopeBar()
404     {
405         if (this._scopeBar) {
406             this._placeholderScopeBarItem = null;
407
408             this._recordingNavigationBar.removeNavigationItem(this._scopeBar);
409             this._scopeBar = null;
410         }
411
412         this._recordingNavigationBar.element.classList.toggle("hidden", !this._canvas);
413
414         let hasRecordings = this._canvas && this._canvas.recordingCollection.size;
415         this.element.classList.toggle("has-recordings", hasRecordings);
416         if (!hasRecordings)
417             return;
418
419         let scopeBarItems = [];
420         let selectedScopeBarItem = null;
421         for (let recording of this._canvas.recordingCollection) {
422             let scopeBarItem = new WI.ScopeBarItem(recording.displayName, recording.displayName);
423             if (recording === this._recording)
424                 selectedScopeBarItem = scopeBarItem;
425             else
426                 scopeBarItem.selected = false;
427             scopeBarItem.__recording = recording;
428             scopeBarItems.push(scopeBarItem);
429         }
430
431         if (!selectedScopeBarItem) {
432             selectedScopeBarItem = scopeBarItems[0];
433
434             const exclusive = true;
435             const className = null;
436             const hidden = true;
437             this._placeholderScopeBarItem = new WI.ScopeBarItem("canvas-recording-scope-bar-item-placeholder", WI.UIString("Recordings"), exclusive, className, hidden);
438             this._placeholderScopeBarItem.selected = true;
439
440             scopeBarItems.unshift(this._placeholderScopeBarItem);
441         }
442
443         this._scopeBar = new WI.ScopeBar("canvas-recordinga-scope-bar", scopeBarItems, selectedScopeBarItem, true);
444         this._scopeBar.addEventListener(WI.ScopeBar.Event.SelectionChanged, this._scopeBarSelectionChanged, this);
445         this._recordingNavigationBar.insertNavigationItem(this._scopeBar, 0);
446     }
447 };
448
449 WI.CanvasSidebarPanel.SelectedActionSymbol = Symbol("selected-action");