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