Web Inspector: Canvas tab: create icons for recordings/shaders in the preview tile
[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             let recordingAction = objects.find((object) => object instanceof WI.RecordingAction);
264             if (recordingAction !== recording[WI.CanvasSidebarPanel.SelectedActionSymbol])
265                 this.action = recordingAction;
266
267             this.recording = recording;
268             return;
269         }
270
271         this.canvas = null;
272         this.recording = null;
273     }
274
275     _treeOutlineSelectionDidChange(event)
276     {
277         let treeElement = event.data.selectedElement;
278         if (!treeElement)
279             return;
280
281         if ((treeElement instanceof WI.CanvasTreeElement) || (treeElement instanceof WI.ShaderProgramTreeElement)) {
282             if (this._placeholderScopeBarItem)
283                 this._placeholderScopeBarItem.selected = true;
284
285             this.showDefaultContentViewForTreeElement(treeElement);
286             return;
287         }
288
289         if (treeElement instanceof WI.FolderTreeElement)
290             treeElement = treeElement.children.lastValue;
291
292         if (!(treeElement instanceof WI.RecordingActionTreeElement))
293             return;
294
295         console.assert(this._recording, "Missing recording for action tree element.", treeElement);
296         this._recording[WI.CanvasSidebarPanel.SelectedActionSymbol] = treeElement.representedObject;
297
298         const onlyExisting = true;
299         let recordingContentView = this.contentBrowser.contentViewForRepresentedObject(this._recording, onlyExisting);
300         if (recordingContentView)
301             recordingContentView.updateActionIndex(treeElement.index);
302     }
303
304     _canvasChanged()
305     {
306         this._canvasTreeOutline.removeChildren();
307
308         if (!this._canvas) {
309             this._recordingNavigationBar.element.classList.add("hidden");
310             return;
311         }
312
313         const showRecordings = false;
314         let canvasTreeElement = new WI.CanvasTreeElement(this._canvas, showRecordings);
315         canvasTreeElement.expanded = true;
316         this._canvasTreeOutline.appendChild(canvasTreeElement);
317
318         const omitFocus = false;
319         const selectedByUser = false;
320         canvasTreeElement.revealAndSelect(omitFocus, selectedByUser);
321
322         if (WI.Canvas.ContextType.Canvas2D || this._canvas.contextType === WI.Canvas.ContextType.WebGL)
323             this._recordButtonNavigationItem.enabled = true;
324
325         this.recording = null;
326     }
327
328     _recordingChanged()
329     {
330         this._recordingTreeOutline.removeChildren();
331
332         if (!this._recording)
333             return;
334
335         if (!this._recordingProcessSpinner) {
336             this._recordingProcessSpinner = new WI.IndeterminateProgressSpinner;
337             this._recordingContentContainer.appendChild(this._recordingProcessSpinner.element);
338         }
339
340         this.contentBrowser.showContentViewForRepresentedObject(this._recording);
341
342         let recording = this._recording;
343
344         let promise = this._recording.process().then(() => {
345             if (recording !== this._recording || promise !== this._recordingProcessPromise)
346                 return;
347
348             this._recordingProcessPromise = null;
349
350             if (this._recordingProcessSpinner) {
351                 this._recordingProcessSpinner.element.remove();
352                 this._recordingProcessSpinner = null;
353             }
354
355             this._recordingTreeOutline.element.dataset.indent = Number.countDigits(this._recording.actions.length);
356
357             if (this._recording.actions[0] instanceof WI.RecordingInitialStateAction)
358                 this._recordingTreeOutline.appendChild(new WI.RecordingActionTreeElement(this._recording.actions[0], 0, this._recording.type));
359
360             let cumulativeActionIndex = 1;
361             this._recording.frames.forEach((frame, frameIndex) => {
362                 let folder = new WI.FolderTreeElement(WI.UIString("Frame %d").format((frameIndex + 1).toLocaleString()));
363                 this._recordingTreeOutline.appendChild(folder);
364
365                 for (let i = 0; i < frame.actions.length; ++i)
366                     folder.appendChild(new WI.RecordingActionTreeElement(frame.actions[i], cumulativeActionIndex + i, this._recording.type));
367
368                 if (!isNaN(frame.duration)) {
369                     const higherResolution = true;
370                     folder.status = Number.secondsToString(frame.duration / 1000, higherResolution);
371                 }
372
373                 if (frame.incomplete)
374                     folder.subtitle = WI.UIString("Incomplete");
375
376                 if (this._recording.frames.length === 1)
377                     folder.expand();
378
379                 cumulativeActionIndex += frame.actions.length;
380             });
381
382             if (this._scopeBar) {
383                 let scopeBarItem = this._scopeBar.item(this._recording.displayName);
384                 console.assert(scopeBarItem, "Missing scopeBarItem for recording.", this._recording);
385                 scopeBarItem.selected = true;
386             }
387
388             this.action = this._recording[WI.CanvasSidebarPanel.SelectedActionSymbol] || this._recording.actions[0];
389         });
390
391         this._recordingProcessPromise = promise;
392     }
393
394     _updateRecordNavigationItem()
395     {
396         if (!this._canvas || !(this._canvas.contextType === WI.Canvas.ContextType.Canvas2D || this._canvas.contextType === WI.Canvas.ContextType.WebGL)) {
397             this._recordButtonNavigationItem.enabled = false;
398             return;
399         }
400
401         let isRecording = this._canvas.isRecording;
402         this._recordButtonNavigationItem.enabled = isRecording || !WI.canvasManager.recordingCanvas;
403         this._recordButtonNavigationItem.toggled = isRecording;
404     }
405
406     _updateRecordingScopeBar()
407     {
408         if (this._scopeBar) {
409             this._placeholderScopeBarItem = null;
410
411             this._recordingNavigationBar.removeNavigationItem(this._scopeBar);
412             this._scopeBar = null;
413         }
414
415         this._recordingNavigationBar.element.classList.toggle("hidden", !this._canvas);
416
417         let hasRecordings = this._canvas && this._canvas.recordingCollection.size;
418         this.element.classList.toggle("has-recordings", hasRecordings);
419         if (!hasRecordings)
420             return;
421
422         let scopeBarItems = [];
423         let selectedScopeBarItem = null;
424         for (let recording of this._canvas.recordingCollection) {
425             let scopeBarItem = new WI.ScopeBarItem(recording.displayName, recording.displayName);
426             if (recording === this._recording)
427                 selectedScopeBarItem = scopeBarItem;
428             else
429                 scopeBarItem.selected = false;
430             scopeBarItem.__recording = recording;
431             scopeBarItems.push(scopeBarItem);
432         }
433
434         if (!selectedScopeBarItem) {
435             selectedScopeBarItem = scopeBarItems[0];
436
437             const exclusive = true;
438             const className = null;
439             const hidden = true;
440             this._placeholderScopeBarItem = new WI.ScopeBarItem("canvas-recording-scope-bar-item-placeholder", WI.UIString("Recordings"), exclusive, className, hidden);
441             this._placeholderScopeBarItem.selected = true;
442
443             scopeBarItems.unshift(this._placeholderScopeBarItem);
444         }
445
446         this._scopeBar = new WI.ScopeBar("canvas-recordinga-scope-bar", scopeBarItems, selectedScopeBarItem, true);
447         this._scopeBar.addEventListener(WI.ScopeBar.Event.SelectionChanged, this._scopeBarSelectionChanged, this);
448         this._recordingNavigationBar.insertNavigationItem(this._scopeBar, 0);
449     }
450 };
451
452 WI.CanvasSidebarPanel.SelectedActionSymbol = Symbol("selected-action");