Web Inspector: REGRESSION: CanvasSidebarPanel is empty for imported recordings
[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._recordingProcessingOptionsContainer = null;
72
73         this._selectedRecordingActionIndex = NaN;
74     }
75
76     // Public
77
78     get canvas()
79     {
80         return this._canvas;
81     }
82
83     set canvas(canvas)
84     {
85         if (this._canvas === canvas)
86             return;
87
88         if (this._canvas)
89             this._canvas.recordingCollection.removeEventListener(null, null, this);
90
91         this._canvas = canvas;
92         if (this._canvas) {
93             this._canvas.recordingCollection.addEventListener(WI.Collection.Event.ItemAdded, this._recordingAdded, this);
94             this._canvas.recordingCollection.addEventListener(WI.Collection.Event.ItemRemoved, this._recordingRemoved, this);
95         }
96
97         this._canvasChanged();
98         this._updateRecordNavigationItem();
99         this._updateRecordingScopeBar();
100     }
101
102     set recording(recording)
103     {
104         if (recording === this._recording)
105             return;
106
107         if (this._recording) {
108             this._recording.removeEventListener(WI.Recording.Event.ProcessedAction, this._handleRecordingProcessedAction, this);
109             this._recording.removeEventListener(WI.Recording.Event.StartProcessingFrame, this._handleRecordingStartProcessingFrame, this);
110         }
111
112         if (recording)
113             this.canvas = recording.source;
114
115         this._recording = recording;
116
117         if (this._recording) {
118             this._recording.addEventListener(WI.Recording.Event.ProcessedAction, this._handleRecordingProcessedAction, this);
119             this._recording.addEventListener(WI.Recording.Event.StartProcessingFrame, this._handleRecordingStartProcessingFrame, this);
120         }
121
122         this._updateRecordNavigationItem();
123         this._updateRecordingScopeBar();
124         this._recordingChanged();
125     }
126
127     set action(action)
128     {
129         if (!this._recording)
130             return;
131
132         if (action === this._recording.actions[this._selectedRecordingActionIndex])
133             return;
134
135         let selectedTreeElement = this._recordingTreeOutline.selectedTreeElement;
136         if (!action) {
137             if (selectedTreeElement)
138                 selectedTreeElement.deselect();
139             return;
140         }
141
142         if (selectedTreeElement && selectedTreeElement instanceof WI.FolderTreeElement) {
143             let lastActionTreeElement = selectedTreeElement.children.lastValue;
144             if (action === lastActionTreeElement.representedObject)
145                 return;
146         }
147
148         let treeElement = this._recordingTreeOutline.findTreeElement(action);
149         console.assert(treeElement, "Missing tree element for recording action.", action);
150         if (!treeElement)
151             return;
152
153         this._recording[WI.CanvasSidebarPanel.SelectedActionSymbol] = action;
154
155         const omitFocus = false;
156         const selectedByUser = false;
157         treeElement.revealAndSelect(omitFocus, selectedByUser);
158
159         this._selectedRecordingActionIndex = this._recording.actions.indexOf(action);
160     }
161
162     shown()
163     {
164         super.shown();
165
166         this.contentBrowser.addEventListener(WI.ContentBrowser.Event.CurrentRepresentedObjectsDidChange, this._currentRepresentedObjectsDidChange, this);
167         this._currentRepresentedObjectsDidChange();
168     }
169
170     hidden()
171     {
172         this.contentBrowser.removeEventListener(null, null, this);
173
174         super.hidden();
175     }
176
177     canShowRepresentedObject(representedObject)
178     {
179         if (representedObject instanceof WI.CanvasCollection)
180             return false;
181
182         return super.canShowRepresentedObject(representedObject);
183     }
184
185     // Protected
186
187     hasCustomFilters()
188     {
189         return true;
190     }
191
192     matchTreeElementAgainstCustomFilters(treeElement)
193     {
194         // Keep recording frame tree elements.
195         if (treeElement instanceof WI.FolderTreeElement)
196             return true;
197
198         // Always show the Initial State tree element.
199         if (treeElement instanceof WI.RecordingActionTreeElement && treeElement.representedObject instanceof WI.RecordingInitialStateAction)
200             return true;
201
202         return super.matchTreeElementAgainstCustomFilters(treeElement);
203     }
204
205     initialLayout()
206     {
207         super.initialLayout();
208
209         let filterFunction = (treeElement) => {
210             if (!(treeElement.representedObject instanceof WI.RecordingAction))
211                 return false;
212
213             return treeElement.representedObject.isVisual || treeElement.representedObject instanceof WI.RecordingInitialStateAction;
214         };
215
216         const activatedByDefault = false;
217         const defaultToolTip = WI.UIString("Only show visual actions");
218         const activatedToolTip = WI.UIString("Show all actions");
219         this.filterBar.addFilterBarButton("recording-show-visual-only", filterFunction, activatedByDefault, defaultToolTip, activatedToolTip, "Images/Paint.svg", 15, 15);
220     }
221
222     // Private
223
224     _recordingAdded(event)
225     {
226         this.recording = event.data.item;
227     }
228
229     _recordingRemoved(event)
230     {
231         this._updateRecordingScopeBar();
232
233         let recording = event.data.item;
234         if (recording === this.recording)
235             this.recording = this._canvas ? Array.from(this._canvas.recordingCollection).lastValue : null;
236     }
237
238     _scopeBarSelectionChanged()
239     {
240         let selectedScopeBarItem = this._scopeBar.selectedItems[0];
241         this.recording = selectedScopeBarItem.__recording || null;
242     }
243
244     _toggleRecording(event)
245     {
246         if (!this._canvas)
247             return;
248
249         if (this._canvas.isRecording)
250             WI.canvasManager.stopRecording();
251         else if (!WI.canvasManager.recordingCanvas) {
252             let singleFrame = event.data.nativeEvent.shiftKey;
253             WI.canvasManager.startRecording(this._canvas, singleFrame);
254         }
255     }
256
257     _currentRepresentedObjectsDidChange(event)
258     {
259         let objects = this.contentBrowser.currentRepresentedObjects;
260
261         let canvas = objects.find((object) => object instanceof WI.Canvas);
262         if (canvas) {
263             this.canvas = canvas;
264             return;
265         }
266
267         let shaderProgram = objects.find((object) => object instanceof WI.ShaderProgram);
268         if (shaderProgram) {
269             this.canvas = shaderProgram.canvas;
270             let treeElement = this._canvasTreeOutline.findTreeElement(shaderProgram);
271             const omitFocus = false;
272             const selectedByUser = false;
273             treeElement.revealAndSelect(omitFocus, selectedByUser);
274             return;
275         }
276
277         let recording = objects.find((object) => object instanceof WI.Recording);
278         if (recording) {
279             this.recording = recording;
280
281             let recordingAction = objects.find((object) => object instanceof WI.RecordingAction);
282             if (recordingAction !== recording[WI.CanvasSidebarPanel.SelectedActionSymbol])
283                 this.action = recordingAction;
284
285             return;
286         }
287
288         this.canvas = null;
289         this.recording = null;
290     }
291
292     _treeOutlineSelectionDidChange(event)
293     {
294         let treeElement = event.data.selectedElement;
295         if (!treeElement)
296             return;
297
298         if ((treeElement instanceof WI.CanvasTreeElement) || (treeElement instanceof WI.ShaderProgramTreeElement)) {
299             if (this._placeholderScopeBarItem)
300                 this._placeholderScopeBarItem.selected = true;
301
302             this.showDefaultContentViewForTreeElement(treeElement);
303             return;
304         }
305
306         if (treeElement instanceof WI.FolderTreeElement)
307             treeElement = treeElement.children.lastValue;
308
309         if (!(treeElement instanceof WI.RecordingActionTreeElement))
310             return;
311
312         console.assert(this._recording, "Missing recording for action tree element.", treeElement);
313         this._recording[WI.CanvasSidebarPanel.SelectedActionSymbol] = treeElement.representedObject;
314
315         const onlyExisting = true;
316         let recordingContentView = this.contentBrowser.contentViewForRepresentedObject(this._recording, onlyExisting);
317         if (!recordingContentView)
318             return;
319
320         this._selectedRecordingActionIndex = treeElement.index;
321         recordingContentView.updateActionIndex(this._selectedRecordingActionIndex);
322     }
323
324     _canvasChanged()
325     {
326         this._canvasTreeOutline.removeChildren();
327
328         if (!this._canvas) {
329             this._recordingNavigationBar.element.classList.add("hidden");
330             return;
331         }
332
333         const showRecordings = false;
334         let canvasTreeElement = new WI.CanvasTreeElement(this._canvas, showRecordings);
335         canvasTreeElement.expanded = true;
336         this._canvasTreeOutline.appendChild(canvasTreeElement);
337
338         const omitFocus = false;
339         const selectedByUser = false;
340         canvasTreeElement.revealAndSelect(omitFocus, selectedByUser);
341
342         if (WI.Canvas.ContextType.Canvas2D || this._canvas.contextType === WI.Canvas.ContextType.WebGL)
343             this._recordButtonNavigationItem.enabled = true;
344
345         this.recording = null;
346     }
347
348     _recordingChanged()
349     {
350         this._recordingTreeOutline.removeChildren();
351
352         if (!this._recording) {
353             if (this._recordingProcessingOptionsContainer) {
354                 this._recordingProcessingOptionsContainer.remove();
355                 this._recordingProcessingOptionsContainer = null;
356             }
357             return;
358         }
359
360         if (!this._recording.ready) {
361             if (!this._recording.processing)
362                 this._recording.startProcessing();
363
364             if (!this._recordingProcessingOptionsContainer) {
365                 this._recordingProcessingOptionsContainer = this._recordingContentContainer.appendChild(document.createElement("div"));
366                 this._recordingProcessingOptionsContainer.classList.add("recording-processing-options");
367
368                 let createPauseButton = () => {
369                     let spinner = new WI.IndeterminateProgressSpinner;
370                     this._recordingProcessingOptionsContainer.appendChild(spinner.element);
371
372                     let pauseButton = this._recordingProcessingOptionsContainer.appendChild(document.createElement("button"));
373                     pauseButton.textContent = WI.UIString("Pause Processing");
374                     pauseButton.addEventListener("click", (event) => {
375                         this._recording.stopProcessing();
376
377                         spinner.element.remove();
378                         pauseButton.remove();
379                         createResumeButton();
380                     });
381                 };
382
383                 let createResumeButton = () => {
384                     let resumeButton = this._recordingProcessingOptionsContainer.appendChild(document.createElement("button"));
385                     resumeButton.textContent = WI.UIString("Resume Processing");
386                     resumeButton.addEventListener("click", (event) => {
387                         this._recording.startProcessing();
388
389                         resumeButton.remove();
390                         createPauseButton();
391                     });
392                 };
393
394                 if (this._recording.processing)
395                     createPauseButton();
396                 else
397                     createResumeButton();
398             }
399         }
400
401         this.contentBrowser.showContentViewForRepresentedObject(this._recording);
402
403         if (this._scopeBar) {
404             let scopeBarItem = this._scopeBar.item(this._recording.displayName);
405             console.assert(scopeBarItem, "Missing scopeBarItem for recording.", this._recording);
406             scopeBarItem.selected = true;
407         }
408
409         let initialStateAction = this._recording.actions[0];
410         if (initialStateAction.ready && !this._recordingTreeOutline.getCachedTreeElement(initialStateAction)) {
411             this._recordingTreeOutline.appendChild(new WI.RecordingActionTreeElement(initialStateAction, 0, this._recording.type));
412
413             if (!this._recording[WI.CanvasSidebarPanel.SelectedActionSymbol])
414                 this.action = initialStateAction;
415         }
416
417         let cumulativeActionIndex = 0;
418         this._recording.frames.forEach((frame, frameIndex) => {
419             if (!frame.actions[0].ready)
420                 return;
421
422             let folder = this._recordingTreeOutline.getCachedTreeElement(frame);
423             if (!folder)
424                 folder = this._createRecordingFrameTreeElement(frame, frameIndex, this._recordingTreeOutline);
425
426             for (let action of frame.actions) {
427                 ++cumulativeActionIndex;
428
429                 if (!action.ready || this._recordingTreeOutline.getCachedTreeElement(action))
430                     break;
431
432                 this._createRecordingActionTreeElement(action, cumulativeActionIndex, folder);
433             }
434         });
435     }
436
437     _updateRecordNavigationItem()
438     {
439         if (!this._canvas || !(this._canvas.contextType === WI.Canvas.ContextType.Canvas2D || this._canvas.contextType === WI.Canvas.ContextType.WebGL)) {
440             this._recordButtonNavigationItem.enabled = false;
441             return;
442         }
443
444         let isRecording = this._canvas.isRecording;
445         this._recordButtonNavigationItem.enabled = isRecording || !WI.canvasManager.recordingCanvas;
446         this._recordButtonNavigationItem.toggled = isRecording;
447     }
448
449     _updateRecordingScopeBar()
450     {
451         if (this._scopeBar) {
452             this._placeholderScopeBarItem = null;
453
454             this._recordingNavigationBar.removeNavigationItem(this._scopeBar);
455             this._scopeBar = null;
456         }
457
458         this._recordingNavigationBar.element.classList.toggle("hidden", !this._canvas);
459
460         let hasRecordings = this._recording || (this._canvas && this._canvas.recordingCollection.size);
461         this.element.classList.toggle("has-recordings", hasRecordings);
462         if (!hasRecordings)
463             return;
464
465         let scopeBarItems = [];
466         let selectedScopeBarItem = null;
467
468         let createScopeBarItem = (recording) => {
469             let scopeBarItem = new WI.ScopeBarItem(recording.displayName, recording.displayName);
470             if (recording === this._recording)
471                 selectedScopeBarItem = scopeBarItem;
472             else
473                 scopeBarItem.selected = false;
474             scopeBarItem.__recording = recording;
475             scopeBarItems.push(scopeBarItem);
476         };
477
478         if (this._canvas && this._canvas.recordingCollection) {
479             for (let recording of this._canvas.recordingCollection)
480                 createScopeBarItem(recording);
481         }
482
483         if (this._recording && (!this._canvas || !this._canvas.recordingCollection.has(this._recording)))
484             createScopeBarItem(this._recording);
485
486         if (!selectedScopeBarItem) {
487             selectedScopeBarItem = scopeBarItems[0];
488
489             const exclusive = true;
490             const className = null;
491             const hidden = true;
492             this._placeholderScopeBarItem = new WI.ScopeBarItem("canvas-recording-scope-bar-item-placeholder", WI.UIString("Recordings"), exclusive, className, hidden);
493             this._placeholderScopeBarItem.selected = true;
494
495             scopeBarItems.unshift(this._placeholderScopeBarItem);
496         }
497
498         this._scopeBar = new WI.ScopeBar("canvas-recordinga-scope-bar", scopeBarItems, selectedScopeBarItem, true);
499         this._scopeBar.addEventListener(WI.ScopeBar.Event.SelectionChanged, this._scopeBarSelectionChanged, this);
500         this._recordingNavigationBar.insertNavigationItem(this._scopeBar, 0);
501     }
502
503     _createRecordingFrameTreeElement(frame, index, parent)
504     {
505         let folder = new WI.FolderTreeElement(WI.UIString("Frame %d").format((index + 1).toLocaleString()), frame);
506
507         if (!isNaN(frame.duration)) {
508             const higherResolution = true;
509             folder.status = Number.secondsToString(frame.duration / 1000, higherResolution);
510         }
511
512         parent.appendChild(folder);
513
514         return folder;
515     }
516
517     _createRecordingActionTreeElement(action, index, parent)
518     {
519         let treeElement = new WI.RecordingActionTreeElement(action, index, this._recording.type);
520
521         parent.appendChild(treeElement);
522
523         if (parent instanceof WI.FolderTreeElement && parent.representedObject instanceof WI.RecordingFrame) {
524             if (action !== parent.representedObject.actions.lastValue) {
525                 parent.addClassName("processing");
526
527                 if (!(parent.subtitle instanceof HTMLProgressElement))
528                     parent.subtitle = document.createElement("progress");
529
530                 if (parent.statusElement)
531                     parent.subtitle.style.setProperty("width", `calc(100% - ${parent.statusElement.offsetWidth + 4}px`);
532
533                 parent.subtitle.value = parent.representedObject.actions.indexOf(action) / parent.representedObject.actions.length;
534             } else {
535                 parent.removeClassName("processing");
536                 if (parent.representedObject.incomplete)
537                     parent.subtitle = WI.UIString("Incomplete");
538                 else
539                     parent.subtitle = "";
540             }
541         }
542
543         if (action === this._recording[WI.CanvasSidebarPanel.SelectedActionSymbol])
544             this.action = action;
545
546         return treeElement;
547     }
548
549     _handleRecordingProcessedAction(event)
550     {
551         let {action, index} = event.data;
552
553         this._recordingTreeOutline.element.dataset.indent = Number.countDigits(index);
554
555         let isInitialStateAction = !index;
556
557         console.assert(isInitialStateAction || this._recordingTreeOutline.children.lastValue instanceof WI.FolderTreeElement, "There should be a WI.FolderTreeElement for the frame for this action.");
558         this._createRecordingActionTreeElement(action, index, isInitialStateAction ? this._recordingTreeOutline : this._recordingTreeOutline.children.lastValue);
559
560         if (isInitialStateAction && !this._recording[WI.CanvasSidebarPanel.SelectedActionSymbol])
561             this.action = action;
562
563         if (action === this._recording.actions.lastValue && this._recordingProcessingOptionsContainer) {
564             this._recordingProcessingOptionsContainer.remove();
565             this._recordingProcessingOptionsContainer = null;
566         }
567     }
568
569     _handleRecordingStartProcessingFrame(event)
570     {
571         let {frame, index} = event.data;
572
573         this._createRecordingFrameTreeElement(frame, index, this._recordingTreeOutline);
574     }
575 };
576
577 WI.CanvasSidebarPanel.SelectedActionSymbol = Symbol("selected-action");