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