Web Inspector: Canvas: auto-record after page load sometimes shows the wrong UI
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / CanvasContentView.js
1 /*
2  * Copyright (C) 2017 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.CanvasContentView = class CanvasContentView extends WI.ContentView
27 {
28     constructor(representedObject)
29     {
30         console.assert(representedObject instanceof WI.Canvas);
31
32         super(representedObject);
33
34         this.element.classList.add("canvas");
35
36         this._progressView = null;
37         this._previewContainerElement = null;
38         this._previewImageElement = null;
39         this._errorElement = null;
40         this._memoryCostElement = null;
41         this._pendingContent = null;
42         this._pixelSize = null;
43         this._pixelSizeElement = null;
44         this._canvasNode = null;
45
46         this._refreshButtonNavigationItem = new WI.ButtonNavigationItem("refresh", WI.UIString("Refresh"), "Images/ReloadFull.svg", 13, 13);
47         this._refreshButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
48         this._refreshButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this.refreshPreview, this);
49
50         this._showGridButtonNavigationItem = new WI.ActivateButtonNavigationItem("show-grid", WI.UIString("Show Grid"), WI.UIString("Hide Grid"), "Images/NavigationItemCheckers.svg", 13, 13);
51         this._showGridButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._showGridButtonClicked, this);
52         this._showGridButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
53         this._showGridButtonNavigationItem.activated = !!WI.settings.showImageGrid.value;
54     }
55
56     // Public
57
58     get navigationItems()
59     {
60         // The toggle recording NavigationItem isn't added to the ContentBrowser's NavigationBar.
61         // It's added to the "quick access" NavigationBar shown when hovering the canvas in the overview.
62         return [this._refreshButtonNavigationItem, this._showGridButtonNavigationItem];
63     }
64
65     refreshPreview()
66     {
67         this._pendingContent = null;
68
69         this.representedObject.requestContent().then((content) => {
70             this._pendingContent = content;
71             if (!this._pendingContent) {
72                 this._showError();
73                 return;
74             }
75
76             this.needsLayout();
77         });
78     }
79
80     // Protected
81
82     initialLayout()
83     {
84         super.initialLayout();
85
86         let isCard = !this._refreshButtonNavigationItem.parentNavigationBar;
87
88         if (isCard) {
89             let header = this.element.appendChild(document.createElement("header"));
90             header.addEventListener("click", (event) => { event.stopPropagation(); });
91
92             let titles = header.appendChild(document.createElement("div"));
93             titles.className = "titles";
94
95             let title = titles.appendChild(document.createElement("span"));
96             title.className = "title";
97             title.textContent = this.representedObject.displayName;
98
99             let subtitle = titles.appendChild(document.createElement("span"));
100             subtitle.className = "subtitle";
101             subtitle.textContent = WI.Canvas.displayNameForContextType(this.representedObject.contextType);
102
103             let navigationBar = new WI.NavigationBar;
104
105             if (this.representedObject.contextType === WI.Canvas.ContextType.Canvas2D || this.representedObject.contextType === WI.Canvas.ContextType.BitmapRenderer || this.representedObject.contextType === WI.Canvas.ContextType.WebGL) {
106                 const toolTip = WI.UIString("Start recording canvas actions.\nShift-click to record a single frame.");
107                 const altToolTip = WI.UIString("Stop recording canvas actions");
108                 this._recordButtonNavigationItem = new WI.ToggleButtonNavigationItem("record-start-stop", toolTip, altToolTip, "Images/Record.svg", "Images/Stop.svg", 13, 13);
109                 this._recordButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.High;
110                 this._recordButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._toggleRecording, this);
111                 navigationBar.addNavigationItem(this._recordButtonNavigationItem);
112             }
113
114             let canvasElementButtonNavigationItem = new WI.ButtonNavigationItem("canvas-element", WI.UIString("Canvas Element"), "Images/Markup.svg", 16, 16);
115             canvasElementButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
116             canvasElementButtonNavigationItem.element.addEventListener("mousedown", this._handleCanvasElementButtonMouseDown.bind(this));
117             navigationBar.addNavigationItem(canvasElementButtonNavigationItem);
118
119             navigationBar.addNavigationItem(this._refreshButtonNavigationItem);
120
121             header.append(navigationBar.element);
122         }
123
124         this._previewContainerElement = this.element.appendChild(document.createElement("div"));
125         this._previewContainerElement.className = "preview";
126
127         if (isCard) {
128             let footer = this.element.appendChild(document.createElement("footer"));
129             footer.addEventListener("click", (event) => { event.stopPropagation(); });
130
131             this._viewRelatedItemsContainer = footer.appendChild(document.createElement("div"));
132             this._viewRelatedItemsContainer.classList.add("view-related-items");
133
134             this._viewShaderButton = document.createElement("img");
135             this._viewShaderButton.classList.add("view-shader");
136             this._viewShaderButton.title = WI.UIString("View Shader");
137             this._viewShaderButton.addEventListener("mousedown", this._handleViewShaderButtonMouseDown.bind(this));
138
139             this._viewRecordingButton = document.createElement("img");
140             this._viewRecordingButton.classList.add("view-recording");
141             this._viewRecordingButton.title = WI.UIString("View Recording");
142             this._viewRecordingButton.addEventListener("mousedown", this._handleViewRecordingButtonMouseDown.bind(this));
143
144             this._updateViewRelatedItems();
145
146             let flexibleSpaceElement = footer.appendChild(document.createElement("div"));
147             flexibleSpaceElement.className = "flexible-space";
148
149             let metrics = footer.appendChild(document.createElement("div"));
150
151             this._pixelSizeElement = metrics.appendChild(document.createElement("span"));
152             this._pixelSizeElement.className = "pixel-size";
153
154             this._memoryCostElement = metrics.appendChild(document.createElement("span"));
155             this._memoryCostElement.className = "memory-cost";
156         }
157
158         if (this._errorElement)
159             this._showError();
160
161         if (isCard)
162             this._refreshPixelSize();
163     }
164
165     layout()
166     {
167         super.layout();
168
169         if (this._pendingContent) {
170             if (this._errorElement) {
171                 this._errorElement.remove();
172                 this._errorElement = null;
173             }
174
175             if (!this._previewImageElement) {
176                 this._previewImageElement = document.createElement("img");
177                 this._previewImageElement.addEventListener("error", this._showError.bind(this));
178             }
179
180             this._previewImageElement.src = this._pendingContent;
181             this._pendingContent = null;
182
183             if (!this._previewImageElement.parentNode)
184                 this._previewContainerElement.appendChild(this._previewImageElement);
185         }
186
187         this._updateRecordNavigationItem();
188         this._updateProgressView();
189         this._updateViewRelatedItems();
190         this._updateMemoryCost();
191         this._updateImageGrid();
192     }
193
194     shown()
195     {
196         super.shown();
197
198         this.refreshPreview();
199     }
200
201     attached()
202     {
203         super.attached();
204
205         this.representedObject.addEventListener(WI.Canvas.Event.MemoryChanged, this._updateMemoryCost, this);
206         this.representedObject.addEventListener(WI.Canvas.Event.RecordingStarted, this.needsLayout, this);
207         this.representedObject.addEventListener(WI.Canvas.Event.RecordingProgress, this.needsLayout, this);
208         this.representedObject.addEventListener(WI.Canvas.Event.RecordingStopped, this.needsLayout, this);
209         this.representedObject.shaderProgramCollection.addEventListener(WI.Collection.Event.ItemAdded, this.needsLayout, this);
210         this.representedObject.shaderProgramCollection.addEventListener(WI.Collection.Event.ItemRemoved, this.needsLayout, this);
211
212         this.representedObject.requestNode().then((node) => {
213             console.assert(!this._canvasNode || this._canvasNode === node);
214             if (this._canvasNode === node)
215                 return;
216
217             this._canvasNode = node;
218             this._canvasNode.addEventListener(WI.DOMNode.Event.AttributeModified, this._refreshPixelSize, this);
219             this._canvasNode.addEventListener(WI.DOMNode.Event.AttributeRemoved, this._refreshPixelSize, this);
220         });
221
222         WI.settings.showImageGrid.addEventListener(WI.Setting.Event.Changed, this._updateImageGrid, this);
223     }
224
225     detached()
226     {
227         this.representedObject.removeEventListener(null, null, this);
228         this.representedObject.shaderProgramCollection.removeEventListener(null, null, this);
229
230         if (this._canvasNode) {
231             this._canvasNode.removeEventListener(null, null, this);
232             this._canvasNode = null;
233         }
234
235         WI.settings.showImageGrid.removeEventListener(null, null, this);
236
237         super.detached();
238     }
239
240     // Private
241
242     _showError()
243     {
244         if (this._previewImageElement)
245             this._previewImageElement.remove();
246
247         if (!this._errorElement) {
248             const isError = true;
249             this._errorElement = WI.createMessageTextView(WI.UIString("No Preview Available"), isError);
250         }
251
252         if (this._previewContainerElement)
253             this._previewContainerElement.appendChild(this._errorElement);
254     }
255
256     _toggleRecording(event)
257     {
258         if (this.representedObject.recordingActive)
259             this.representedObject.stopRecording();
260         else {
261             let singleFrame = event.data.nativeEvent.shiftKey;
262             this.representedObject.startRecording(singleFrame);
263         }
264     }
265
266     _refreshPixelSize()
267     {
268         let updatePixelSize = (size) => {
269             if (Object.shallowEqual(this._pixelSize, size))
270                 return;
271
272             this._pixelSize = size;
273
274             if (this._pixelSizeElement) {
275                 if (this._pixelSize)
276                     this._pixelSizeElement.textContent = `${this._pixelSize.width} ${multiplicationSign} ${this._pixelSize.height}`;
277                 else
278                     this._pixelSizeElement.textContent = emDash;
279             }
280
281             this.refreshPreview();
282         };
283
284         this.representedObject.requestSize()
285         .then((size) => {
286             updatePixelSize(size);
287         })
288         .catch((error) => {
289             updatePixelSize(null);
290         });
291     }
292
293     _handleCanvasElementButtonMouseDown(event)
294     {
295         if (this._ignoreCanvasElementButtonMouseDown)
296             return;
297
298         this._ignoreCanvasElementButtonMouseDown = true;
299
300         let contextMenu = WI.ContextMenu.createFromEvent(event);
301         contextMenu.addBeforeShowCallback(() => {
302             this._ignoreCanvasElementButtonMouseDown = false;
303         });
304
305         if (this._canvasNode)
306             WI.appendContextMenuItemsForDOMNode(contextMenu, this._canvasNode);
307
308         contextMenu.appendSeparator();
309
310         contextMenu.appendItem(WI.UIString("Log Canvas Context"), () => {
311             WI.RemoteObject.resolveCanvasContext(this.representedObject, WI.RuntimeManager.ConsoleObjectGroup, (remoteObject) => {
312                 if (!remoteObject)
313                     return;
314
315                 const text = WI.UIString("Selected Canvas Context");
316                 const addSpecialUserLogClass = true;
317                 WI.consoleLogViewController.appendImmediateExecutionWithResult(text, remoteObject, addSpecialUserLogClass);
318             });
319         });
320
321         contextMenu.show();
322     }
323
324     _showGridButtonClicked()
325     {
326         WI.settings.showImageGrid.value = !this._showGridButtonNavigationItem.activated;
327     }
328
329     _updateImageGrid()
330     {
331         let activated = WI.settings.showImageGrid.value;
332         this._showGridButtonNavigationItem.activated = activated;
333
334         if (this._previewImageElement)
335             this._previewImageElement.classList.toggle("show-grid", activated);
336     }
337
338     _updateMemoryCost()
339     {
340         if (!this._memoryCostElement)
341             return;
342
343         let memoryCost = this.representedObject.memoryCost;
344         if (isNaN(memoryCost))
345             this._memoryCostElement.textContent = emDash;
346         else {
347             const higherResolution = false;
348             let bytesString = Number.bytesToString(memoryCost, higherResolution);
349             this._memoryCostElement.textContent = `(${bytesString})`;
350         }
351     }
352
353     _updateRecordNavigationItem()
354     {
355         if (!this._recordButtonNavigationItem)
356             return;
357
358         let recordingActive = this.representedObject.recordingActive;
359         this._recordButtonNavigationItem.toggled = recordingActive;
360         this._refreshButtonNavigationItem.enabled = !recordingActive;
361         this.element.classList.toggle("recording-active", recordingActive);
362     }
363
364     _updateProgressView()
365     {
366         if (!this._previewContainerElement)
367             return;
368
369         if (!this.representedObject.recordingActive) {
370             if (this._progressView && this._progressView.parentView) {
371                 this.removeSubview(this._progressView);
372                 this._progressView = null;
373             }
374             return;
375         }
376
377         if (!this._progressView) {
378             this._progressView = new WI.ProgressView;
379             this.element.insertBefore(this._progressView.element, this._previewContainerElement);
380             this.addSubview(this._progressView);
381         }
382
383         let title = null;
384         if (this.representedObject.recordingFrameCount) {
385             let formatString = this.representedObject.recordingFrameCount === 1 ? WI.UIString("%d Frame") : WI.UIString("%d Frames");
386             title = formatString.format(this.representedObject.recordingFrameCount);
387         } else
388             title = WI.UIString("Waiting for frames\u2026");
389
390         this._progressView.title = title;
391         this._progressView.subtitle = this.representedObject.recordingBufferUsed ? Number.bytesToString(this.representedObject.recordingBufferUsed) : "";
392     }
393
394     _updateViewRelatedItems()
395     {
396         if (!this._viewRelatedItemsContainer)
397             return;
398
399         this._viewRelatedItemsContainer.removeChildren();
400
401         if (this.representedObject.shaderProgramCollection.size)
402             this._viewRelatedItemsContainer.appendChild(this._viewShaderButton);
403
404         if (this.representedObject.recordingCollection.size)
405             this._viewRelatedItemsContainer.appendChild(this._viewRecordingButton);
406     }
407
408     _handleViewShaderButtonMouseDown(event)
409     {
410         if (this._ignoreViewShaderButtonMouseDown)
411             return;
412
413         let shaderPrograms = this.representedObject.shaderProgramCollection;
414         console.assert(shaderPrograms.size);
415         if (!shaderPrograms.size)
416             return;
417
418         if (shaderPrograms.size === 1) {
419             WI.showRepresentedObject(Array.from(shaderPrograms)[0]);
420             return;
421         }
422
423         this._ignoreViewShaderButtonMouseDown = true;
424
425         let contextMenu = WI.ContextMenu.createFromEvent(event);
426         contextMenu.addBeforeShowCallback(() => {
427             this._ignoreViewShaderButtonMouseDown = false;
428         });
429
430         for (let shaderProgram of shaderPrograms) {
431             contextMenu.appendItem(shaderProgram.displayName, () => {
432                 WI.showRepresentedObject(shaderProgram);
433             });
434         }
435
436         contextMenu.show();
437     }
438
439     _handleViewRecordingButtonMouseDown(event)
440     {
441         if (this._ignoreViewRecordingButtonMouseDown)
442             return;
443
444         let recordings = this.representedObject.recordingCollection;
445         console.assert(recordings.size);
446         if (!recordings.size)
447             return;
448
449         if (recordings.size === 1) {
450             WI.showRepresentedObject(Array.from(recordings)[0]);
451             return;
452         }
453
454         this._ignoreViewRecordingButtonMouseDown = true;
455
456         let contextMenu = WI.ContextMenu.createFromEvent(event);
457         contextMenu.addBeforeShowCallback(() => {
458             this._ignoreViewRecordingButtonMouseDown = false;
459         });
460
461         for (let recording of recordings) {
462             contextMenu.appendItem(recording.displayName, () => {
463                 WI.showRepresentedObject(recording);
464             });
465         }
466
467         contextMenu.show();
468     }
469 };