Upstream version 5.34.104.0
[platform/framework/web/crosswalk.git] / src / third_party / WebKit / Source / devtools / front_end / ScreencastView.js
1 /*
2  * Copyright (C) 2013 Google 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 are
6  * met:
7  *
8  *     * Redistributions of source code must retain the above copyright
9  * notice, this list of conditions and the following disclaimer.
10  *     * Redistributions in binary form must reproduce the above
11  * copyright notice, this list of conditions and the following disclaimer
12  * in the documentation and/or other materials provided with the
13  * distribution.
14  *     * Neither the name of Google Inc. nor the names of its
15  * contributors may be used to endorse or promote products derived from
16  * this software without specific prior written permission.
17  *
18  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29  */
30
31 /**
32  * @constructor
33  * @extends {WebInspector.View}
34  * @implements {WebInspector.DOMNodeHighlighter}
35  * @param {!Element} statusBarButtonPlaceholder
36  */
37 WebInspector.ScreencastView = function(statusBarButtonPlaceholder)
38 {
39     WebInspector.View.call(this);
40     this.registerRequiredCSS("screencastView.css");
41     this._statusBarButtonPlaceholder = statusBarButtonPlaceholder;
42 }
43
44 WebInspector.ScreencastView._bordersSize = 40;
45
46 WebInspector.ScreencastView._navBarHeight = 29;
47
48 WebInspector.ScreencastView._HttpRegex = /^https?:\/\/(.+)/;
49
50 WebInspector.ScreencastView.prototype = {
51     initialize: function()
52     {
53         this.element.classList.add("screencast");
54
55         this._createNavigationBar();
56
57         this._viewportElement = this.element.createChild("div", "screencast-viewport hidden");
58         this._glassPaneElement = this.element.createChild("div", "screencast-glasspane hidden");
59
60         this._canvasElement = this._viewportElement.createChild("canvas");
61         this._canvasElement.tabIndex = 1;
62         this._canvasElement.addEventListener("mousedown", this._handleMouseEvent.bind(this), false);
63         this._canvasElement.addEventListener("mouseup", this._handleMouseEvent.bind(this), false);
64         this._canvasElement.addEventListener("mousemove", this._handleMouseEvent.bind(this), false);
65         this._canvasElement.addEventListener("mousewheel", this._handleMouseEvent.bind(this), false);
66         this._canvasElement.addEventListener("click", this._handleMouseEvent.bind(this), false);
67         this._canvasElement.addEventListener("contextmenu", this._handleContextMenuEvent.bind(this), false);
68         this._canvasElement.addEventListener("keydown", this._handleKeyEvent.bind(this), false);
69         this._canvasElement.addEventListener("keyup", this._handleKeyEvent.bind(this), false);
70         this._canvasElement.addEventListener("keypress", this._handleKeyEvent.bind(this), false);
71
72         this._titleElement = this._viewportElement.createChild("div", "screencast-element-title monospace hidden");
73         this._tagNameElement = this._titleElement.createChild("span", "screencast-tag-name");
74         this._nodeIdElement = this._titleElement.createChild("span", "screencast-node-id");
75         this._classNameElement = this._titleElement.createChild("span", "screencast-class-name");
76         this._titleElement.appendChild(document.createTextNode(" "));
77         this._nodeWidthElement = this._titleElement.createChild("span");
78         this._titleElement.createChild("span", "screencast-px").textContent = "px";
79         this._titleElement.appendChild(document.createTextNode(" \u00D7 "));
80         this._nodeHeightElement = this._titleElement.createChild("span");
81         this._titleElement.createChild("span", "screencast-px").textContent = "px";
82
83         this._imageElement = new Image();
84         this._isCasting = false;
85         this._context = this._canvasElement.getContext("2d");
86         this._checkerboardPattern = this._createCheckerboardPattern(this._context);
87
88         this._shortcuts = /** !Object.<number, function(Event=):boolean> */ ({});
89         this._shortcuts[WebInspector.KeyboardShortcut.makeKey("l", WebInspector.KeyboardShortcut.Modifiers.Ctrl)] = this._focusNavigationBar.bind(this);
90
91         WebInspector.resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.ScreencastFrame, this._screencastFrame, this);
92         WebInspector.resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.ScreencastVisibilityChanged, this._screencastVisibilityChanged, this);
93
94         WebInspector.timelineManager.addEventListener(WebInspector.TimelineManager.EventTypes.TimelineStarted, this._onTimeline.bind(this, true), this);
95         WebInspector.timelineManager.addEventListener(WebInspector.TimelineManager.EventTypes.TimelineStopped, this._onTimeline.bind(this, false), this);
96         this._timelineActive = WebInspector.timelineManager.isStarted();
97
98         WebInspector.cpuProfilerModel.addEventListener(WebInspector.CPUProfilerModel.EventTypes.ProfileStarted, this._onProfiler.bind(this, true), this);
99         WebInspector.cpuProfilerModel.addEventListener(WebInspector.CPUProfilerModel.EventTypes.ProfileStopped, this._onProfiler.bind(this, false), this);
100         this._profilerActive = WebInspector.cpuProfilerModel.isRecordingProfile();
101
102         this._updateGlasspane();
103
104         this._currentScreencastState = WebInspector.settings.createSetting("currentScreencastState", "");
105         this._lastScreencastState = WebInspector.settings.createSetting("lastScreencastState", "");
106         this._toggleScreencastButton = new WebInspector.StatusBarStatesSettingButton(
107             "screencast-status-bar-item",
108             ["disabled", "left", "top"],
109             [WebInspector.UIString("Disable screencast."), WebInspector.UIString("Switch to portrait screencast."), WebInspector.UIString("Switch to landscape screencast.")],
110             this._currentScreencastState,
111             this._lastScreencastState,
112             this._toggleScreencastButtonClicked.bind(this));
113         this._statusBarButtonPlaceholder.parentElement.insertBefore(this._toggleScreencastButton.element, this._statusBarButtonPlaceholder);
114         this._statusBarButtonPlaceholder.parentElement.removeChild(this._statusBarButtonPlaceholder);
115     },
116
117     /**
118      * @param {string} state
119      */
120     _toggleScreencastButtonClicked: function(state)
121     {
122         if (state === "disabled")
123             WebInspector.inspectorView.hideScreencastView();
124         else
125             WebInspector.inspectorView.showScreencastView(this, state === "left");
126     },
127
128     wasShown: function()
129     {
130         this._startCasting();
131     },
132
133     willHide: function()
134     {
135         this._stopCasting();
136     },
137
138     _startCasting: function()
139     {
140         if (this._timelineActive || this._profilerActive)
141             return;
142         if (this._isCasting)
143             return;
144         this._isCasting = true;
145
146         const maxImageDimension = 1024;
147         var dimensions = this._viewportDimensions();
148         if (dimensions.width < 0 || dimensions.height < 0) {
149             this._isCasting = false;
150             return;
151         }
152         dimensions.width *= WebInspector.zoomManager.zoomFactor();
153         dimensions.height *= WebInspector.zoomManager.zoomFactor();
154         PageAgent.startScreencast("jpeg", 80, Math.min(maxImageDimension, dimensions.width), Math.min(maxImageDimension, dimensions.height));
155         WebInspector.domAgent.setHighlighter(this);
156     },
157
158     _stopCasting: function()
159     {
160         if (!this._isCasting)
161             return;
162         this._isCasting = false;
163         PageAgent.stopScreencast();
164         WebInspector.domAgent.setHighlighter(null);
165     },
166
167     /**
168      * @param {!WebInspector.Event} event
169      */
170     _screencastFrame: function(event)
171     {
172         var metadata = /** type {PageAgent.ScreencastFrameMetadata} */(event.data.metadata);
173
174         if (!metadata.deviceScaleFactor) {
175           console.log(event.data.data);
176           return;
177         }
178
179         var base64Data = /** type {string} */(event.data.data);
180         this._imageElement.src = "data:image/jpg;base64," + base64Data;
181         this._deviceScaleFactor = metadata.deviceScaleFactor;
182         this._pageScaleFactor = metadata.pageScaleFactor;
183         this._viewport = metadata.viewport;
184         if (!this._viewport)
185             return;
186         var offsetTop = metadata.offsetTop || 0;
187         var offsetBottom = metadata.offsetBottom || 0;
188
189         var screenWidthDIP = this._viewport.width * this._pageScaleFactor;
190         var screenHeightDIP = this._viewport.height * this._pageScaleFactor + offsetTop + offsetBottom;
191         this._screenOffsetTop = offsetTop;
192         this._resizeViewport(screenWidthDIP, screenHeightDIP);
193
194         this._imageZoom = this._imageElement.naturalWidth ? this._canvasElement.offsetWidth / this._imageElement.naturalWidth : 1;
195         this.highlightDOMNode(this._highlightNodeId, this._highlightConfig);
196     },
197
198     _isGlassPaneActive: function()
199     {
200         return !this._glassPaneElement.classList.contains("hidden");
201     },
202
203     /**
204      * @param {!WebInspector.Event} event
205      */
206     _screencastVisibilityChanged: function(event)
207     {
208         this._targetInactive = !event.data.visible;
209         this._updateGlasspane();
210     },
211
212     /**
213      * @param {boolean} on
214      * @private
215      */
216     _onTimeline: function(on)
217     {
218         this._timelineActive = on;
219         if (this._timelineActive)
220             this._stopCasting();
221         else
222             this._startCasting();
223         this._updateGlasspane();
224     },
225
226     /**
227      * @param {boolean} on
228      * @private
229      */
230     _onProfiler: function(on, event) {
231         this._profilerActive = on;
232         if (this._profilerActive)
233             this._stopCasting();
234         else
235             this._startCasting();
236         this._updateGlasspane();
237     },
238
239     _updateGlasspane: function()
240     {
241         if (this._targetInactive) {
242             this._glassPaneElement.textContent = WebInspector.UIString("The tab is inactive");
243             this._glassPaneElement.classList.remove("hidden");
244         } else if (this._timelineActive) {
245             this._glassPaneElement.textContent = WebInspector.UIString("Timeline is active");
246             this._glassPaneElement.classList.remove("hidden");
247         } else if (this._profilerActive) {
248             this._glassPaneElement.textContent = WebInspector.UIString("CPU profiler is active");
249             this._glassPaneElement.classList.remove("hidden");
250         } else {
251             this._glassPaneElement.classList.add("hidden");
252         }
253     },
254
255     /**
256      * @param {number} screenWidthDIP
257      * @param {number} screenHeightDIP
258      */
259     _resizeViewport: function(screenWidthDIP, screenHeightDIP)
260     {
261         var dimensions = this._viewportDimensions();
262         this._screenZoom = Math.min(dimensions.width / screenWidthDIP, dimensions.height / screenHeightDIP);
263
264         var bordersSize = WebInspector.ScreencastView._bordersSize;
265         this._viewportElement.classList.remove("hidden");
266         this._viewportElement.style.width = screenWidthDIP * this._screenZoom + bordersSize + "px";
267         this._viewportElement.style.height = screenHeightDIP * this._screenZoom + bordersSize + "px";
268     },
269
270     /**
271      * @param {!Event} event
272      */
273     _handleMouseEvent: function(event)
274     {
275         if (this._isGlassPaneActive()) {
276           event.consume();
277           return;
278         }
279
280         if (!this._viewport)
281             return;
282
283         if (!this._inspectModeConfig || event.type === "mousewheel") {
284             this._simulateTouchGestureForMouseEvent(event);
285             event.preventDefault();
286             if (event.type === "mousedown")
287                 this._canvasElement.focus();
288             return;
289         }
290
291         var position = this._convertIntoScreenSpace(event);
292         DOMAgent.getNodeForLocation(position.x / this._pageScaleFactor, position.y / this._pageScaleFactor, callback.bind(this));
293
294         /**
295          * @param {?Protocol.Error} error
296          * @param {number} nodeId
297          * @this {WebInspector.ScreencastView}
298          */
299         function callback(error, nodeId)
300         {
301             if (error)
302                 return;
303             if (event.type === "mousemove")
304                 this.highlightDOMNode(nodeId, this._inspectModeConfig);
305             else if (event.type === "click")
306                 WebInspector.domAgent.dispatchEventToListeners(WebInspector.DOMAgent.Events.InspectNodeRequested, nodeId);
307         }
308     },
309
310     /**
311      * @param {!KeyboardEvent} event
312      */
313     _handleKeyEvent: function(event)
314     {
315         if (this._isGlassPaneActive()) {
316             event.consume();
317             return;
318         }
319
320         var shortcutKey = WebInspector.KeyboardShortcut.makeKeyFromEvent(event);
321         var handler = this._shortcuts[shortcutKey];
322         if (handler && handler(event)) {
323             event.consume();
324             return;
325         }
326
327         var type;
328         switch (event.type) {
329         case "keydown": type = "keyDown"; break;
330         case "keyup": type = "keyUp"; break;
331         case "keypress": type = "char"; break;
332         default: return;
333         }
334
335         var text = event.type === "keypress" ? String.fromCharCode(event.charCode) : undefined;
336         InputAgent.dispatchKeyEvent(type, this._modifiersForEvent(event), event.timeStamp / 1000, text, text ? text.toLowerCase() : undefined,
337                                     event.keyIdentifier, event.keyCode /* windowsVirtualKeyCode */, event.keyCode /* nativeVirtualKeyCode */, undefined /* macCharCode */, false, false, false);
338         event.consume();
339         this._canvasElement.focus();
340     },
341
342     /**
343      * @param {!Event} event
344      */
345     _handleContextMenuEvent: function(event)
346     {
347         event.consume(true);
348     },
349
350     /**
351      * @param {!Event} event
352      */
353     _simulateTouchGestureForMouseEvent: function(event)
354     {
355         var position = this._convertIntoScreenSpace(event);
356         var timeStamp = event.timeStamp / 1000;
357         var x = position.x;
358         var y = position.y;
359
360         switch (event.which) {
361         case 1: // Left
362             if (event.type === "mousedown") {
363                 InputAgent.dispatchGestureEvent("scrollBegin", x, y, timeStamp);
364             } else if (event.type === "mousemove") {
365                 var dx = this._lastScrollPosition ? position.x - this._lastScrollPosition.x : 0;
366                 var dy = this._lastScrollPosition ? position.y - this._lastScrollPosition.y : 0;
367                 if (dx || dy)
368                     InputAgent.dispatchGestureEvent("scrollUpdate", x, y, timeStamp, dx, dy);
369             } else if (event.type === "mouseup") {
370                 InputAgent.dispatchGestureEvent("scrollEnd", x, y, timeStamp);
371             } else if (event.type === "mousewheel") {
372                 if (event.altKey) {
373                     var factor = 1.1;
374                     var scale = event.wheelDeltaY < 0 ? 1 / factor : factor;
375                     InputAgent.dispatchGestureEvent("pinchBegin", x, y, timeStamp);
376                     InputAgent.dispatchGestureEvent("pinchUpdate", x, y, timeStamp, 0, 0, scale);
377                     InputAgent.dispatchGestureEvent("pinchEnd", x, y, timeStamp);
378                 } else {
379                     InputAgent.dispatchGestureEvent("scrollBegin", x, y, timeStamp);
380                     InputAgent.dispatchGestureEvent("scrollUpdate", x, y, timeStamp, event.wheelDeltaX, event.wheelDeltaY);
381                     InputAgent.dispatchGestureEvent("scrollEnd", x, y, timeStamp);
382                 }
383             } else if (event.type === "click") {
384                 InputAgent.dispatchMouseEvent("mousePressed", x, y, 0, timeStamp, "left", 1, true);
385                 InputAgent.dispatchMouseEvent("mouseReleased", x, y, 0, timeStamp, "left", 1, true);
386                 // FIXME: migrate to tap once it dispatches clicks again.
387                 // InputAgent.dispatchGestureEvent("tapDown", x, y, timeStamp);
388                 // InputAgent.dispatchGestureEvent("tap", x, y, timeStamp);
389             }
390             this._lastScrollPosition = position;
391             break;
392
393         case 2: // Middle
394             if (event.type === "mousedown") {
395                 InputAgent.dispatchGestureEvent("tapDown", x, y, timeStamp);
396             } else if (event.type === "mouseup") {
397                 InputAgent.dispatchGestureEvent("tap", x, y, timeStamp);
398             }
399             break;
400
401         case 3: // Right
402             if (event.type === "mousedown") {
403                 this._pinchStart = position;
404                 InputAgent.dispatchGestureEvent("pinchBegin", x, y, timeStamp);
405             } else if (event.type === "mousemove") {
406                 var dx = this._pinchStart ? position.x - this._pinchStart.x : 0;
407                 var dy = this._pinchStart ? position.y - this._pinchStart.y : 0;
408                 if (dx || dy) {
409                     var scale = Math.pow(dy < 0 ? 0.999 : 1.001, Math.abs(dy));
410                     InputAgent.dispatchGestureEvent("pinchUpdate", this._pinchStart.x, this._pinchStart.y, timeStamp, 0, 0, scale);
411                 }
412             } else if (event.type === "mouseup") {
413                 InputAgent.dispatchGestureEvent("pinchEnd", x, y, timeStamp);
414             }
415             break;
416         case 0: // None
417         default:
418         }
419     },
420
421     /**
422      * @param {!Event} event
423      * @return {!{x: number, y: number}}
424      */
425     _convertIntoScreenSpace: function(event)
426     {
427         var zoom = this._canvasElement.offsetWidth / this._viewport.width / this._pageScaleFactor;
428         var position  = {};
429         position.x = Math.round(event.offsetX / zoom);
430         position.y = Math.round(event.offsetY / zoom - this._screenOffsetTop);
431         return position;
432     },
433
434     /**
435      * @param {!Event} event
436      * @return number
437      */
438     _modifiersForEvent: function(event)
439     {
440         var modifiers = 0;
441         if (event.altKey)
442             modifiers = 1;
443         if (event.ctrlKey)
444             modifiers += 2;
445         if (event.metaKey)
446             modifiers += 4;
447         if (event.shiftKey)
448             modifiers += 8;
449         return modifiers;
450     },
451
452     onResize: function()
453     {
454         if (this._deferredCasting) {
455             clearTimeout(this._deferredCasting);
456             delete this._deferredCasting;
457         }
458
459         this._stopCasting();
460         this._deferredCasting = setTimeout(this._startCasting.bind(this), 100);
461     },
462
463     /**
464      * @param {!DOMAgent.NodeId} nodeId
465      * @param {?DOMAgent.HighlightConfig} config
466      * @param {!RuntimeAgent.RemoteObjectId=} objectId
467      */
468     highlightDOMNode: function(nodeId, config, objectId)
469     {
470         this._highlightNodeId = nodeId;
471         this._highlightConfig = config;
472         if (!nodeId) {
473             this._model = null;
474             this._config = null;
475             this._node = null;
476             this._titleElement.classList.add("hidden");
477             this._repaint();
478             return;
479         }
480
481         this._node = WebInspector.domAgent.nodeForId(nodeId);
482         DOMAgent.getBoxModel(nodeId, callback.bind(this));
483
484         /**
485          * @param {?Protocol.Error} error
486          * @param {!DOMAgent.BoxModel} model
487          * @this {WebInspector.ScreencastView}
488          */
489         function callback(error, model)
490         {
491             if (error) {
492                 this._repaint();
493                 return;
494             }
495             this._model = this._scaleModel(model);
496             this._config = config;
497             this._repaint();
498         }
499     },
500
501     /**
502      * @param {!DOMAgent.BoxModel} model
503      * @return {!DOMAgent.BoxModel}
504      */
505     _scaleModel: function(model)
506     {
507         var scale = this._canvasElement.offsetWidth / this._viewport.width;
508
509         /**
510          * @param {!DOMAgent.Quad} quad
511          * @this {WebInspector.ScreencastView}
512          */
513         function scaleQuad(quad)
514         {
515             for (var i = 0; i < quad.length; i += 2) {
516                 quad[i] = (quad[i] - this._viewport.x) * scale;
517                 quad[i + 1] = (quad[i + 1] - this._viewport.y) * scale + this._screenOffsetTop * this._screenZoom;
518             }
519         }
520
521         scaleQuad.call(this, model.content);
522         scaleQuad.call(this, model.padding);
523         scaleQuad.call(this, model.border);
524         scaleQuad.call(this, model.margin);
525         return model;
526     },
527
528     _repaint: function()
529     {
530         var model = this._model;
531         var config = this._config;
532
533         this._canvasElement.width = window.devicePixelRatio * this._canvasElement.offsetWidth;
534         this._canvasElement.height = window.devicePixelRatio * this._canvasElement.offsetHeight;
535
536         this._context.save();
537         this._context.scale(window.devicePixelRatio, window.devicePixelRatio);
538
539         // Paint top and bottom gutter.
540         this._context.save();
541         this._context.fillStyle = this._checkerboardPattern;
542         this._context.fillRect(0, 0, this._canvasElement.offsetWidth, this._screenOffsetTop * this._screenZoom);
543         this._context.fillRect(0, this._screenOffsetTop * this._screenZoom + this._imageElement.naturalHeight * this._imageZoom, this._canvasElement.offsetWidth, this._canvasElement.offsetHeight);
544         this._context.restore();
545
546         if (model && config) {
547             this._context.save();
548             const transparentColor = "rgba(0, 0, 0, 0)";
549             var hasContent = model.content && config.contentColor !== transparentColor;
550             var hasPadding = model.padding && config.paddingColor !== transparentColor;
551             var hasBorder = model.border && config.borderColor !== transparentColor;
552             var hasMargin = model.margin && config.marginColor !== transparentColor;
553
554             var clipQuad;
555             if (hasMargin && (!hasBorder || !this._quadsAreEqual(model.margin, model.border))) {
556                 this._drawOutlinedQuadWithClip(model.margin, model.border, config.marginColor);
557                 clipQuad = model.border;
558             }
559             if (hasBorder && (!hasPadding || !this._quadsAreEqual(model.border, model.padding))) {
560                 this._drawOutlinedQuadWithClip(model.border, model.padding, config.borderColor);
561                 clipQuad = model.padding;
562             }
563             if (hasPadding && (!hasContent || !this._quadsAreEqual(model.padding, model.content))) {
564                 this._drawOutlinedQuadWithClip(model.padding, model.content, config.paddingColor);
565                 clipQuad = model.content;
566             }
567             if (hasContent)
568                 this._drawOutlinedQuad(model.content, config.contentColor);
569             this._context.restore();
570
571             this._drawElementTitle();
572
573             this._context.globalCompositeOperation = "destination-over";
574         }
575
576         this._context.drawImage(this._imageElement, 0, this._screenOffsetTop * this._screenZoom, this._imageElement.naturalWidth * this._imageZoom, this._imageElement.naturalHeight * this._imageZoom);
577
578         this._context.restore();
579     },
580
581
582     /**
583      * @param {!DOMAgent.Quad} quad1
584      * @param {!DOMAgent.Quad} quad2
585      * @return {boolean}
586      */
587     _quadsAreEqual: function(quad1, quad2)
588     {
589         for (var i = 0; i < quad1.length; ++i) {
590             if (quad1[i] !== quad2[i])
591                 return false;
592         }
593         return true;
594     },
595
596     /**
597      * @param {!DOMAgent.RGBA} color
598      * @return {string}
599      */
600     _cssColor: function(color)
601     {
602         if (!color)
603             return "transparent";
604         return WebInspector.Color.fromRGBA([color.r, color.g, color.b, color.a]).toString(WebInspector.Color.Format.RGBA) || "";
605     },
606
607     /**
608      * @param {!DOMAgent.Quad} quad
609      * @return {!CanvasRenderingContext2D}
610      */
611     _quadToPath: function(quad)
612     {
613         this._context.beginPath();
614         this._context.moveTo(quad[0], quad[1]);
615         this._context.lineTo(quad[2], quad[3]);
616         this._context.lineTo(quad[4], quad[5]);
617         this._context.lineTo(quad[6], quad[7]);
618         this._context.closePath();
619         return this._context;
620     },
621
622     /**
623      * @param {!DOMAgent.Quad} quad
624      * @param {!DOMAgent.RGBA} fillColor
625      */
626     _drawOutlinedQuad: function(quad, fillColor)
627     {
628         this._context.save();
629         this._context.lineWidth = 2;
630         this._quadToPath(quad).clip();
631         this._context.fillStyle = this._cssColor(fillColor);
632         this._context.fill();
633         this._context.restore();
634     },
635
636     /**
637      * @param {!DOMAgent.Quad} quad
638      * @param {!DOMAgent.Quad} clipQuad
639      * @param {!DOMAgent.RGBA} fillColor
640      */
641     _drawOutlinedQuadWithClip: function (quad, clipQuad, fillColor)
642     {
643         this._context.fillStyle = this._cssColor(fillColor);
644         this._context.save();
645         this._context.lineWidth = 0;
646         this._quadToPath(quad).fill();
647         this._context.globalCompositeOperation = "destination-out";
648         this._context.fillStyle = "red";
649         this._quadToPath(clipQuad).fill();
650         this._context.restore();
651     },
652
653     _drawElementTitle: function()
654     {
655         if (!this._node)
656             return;
657
658         var canvasWidth = this._canvasElement.offsetWidth;
659         var canvasHeight = this._canvasElement.offsetHeight;
660
661         var lowerCaseName = this._node.localName() || this._node.nodeName().toLowerCase();
662         this._tagNameElement.textContent = lowerCaseName;
663         this._nodeIdElement.textContent = this._node.getAttribute("id") ? "#" + this._node.getAttribute("id") : "";
664         this._nodeIdElement.textContent = this._node.getAttribute("id") ? "#" + this._node.getAttribute("id") : "";
665         var className = this._node.getAttribute("class");
666         if (className && className.length > 50)
667            className = className.substring(0, 50) + "\u2026";
668         this._classNameElement.textContent = className || "";
669         this._nodeWidthElement.textContent = this._model.width;
670         this._nodeHeightElement.textContent = this._model.height;
671
672         var marginQuad = this._model.margin;
673         var titleWidth = this._titleElement.offsetWidth + 6;
674         var titleHeight = this._titleElement.offsetHeight + 4;
675
676         var anchorTop = this._model.margin[1];
677         var anchorBottom = this._model.margin[7];
678
679         const arrowHeight = 7;
680         var renderArrowUp = false;
681         var renderArrowDown = false;
682
683         var boxX = Math.max(2, this._model.margin[0]);
684         if (boxX + titleWidth > canvasWidth)
685             boxX = canvasWidth - titleWidth - 2;
686
687         var boxY;
688         if (anchorTop > canvasHeight) {
689             boxY = canvasHeight - titleHeight - arrowHeight;
690             renderArrowDown = true;
691         } else if (anchorBottom < 0) {
692             boxY = arrowHeight;
693             renderArrowUp = true;
694         } else if (anchorBottom + titleHeight + arrowHeight < canvasHeight) {
695             boxY = anchorBottom + arrowHeight - 4;
696             renderArrowUp = true;
697         } else if (anchorTop - titleHeight - arrowHeight > 0) {
698             boxY = anchorTop - titleHeight - arrowHeight + 3;
699             renderArrowDown = true;
700         } else
701             boxY = arrowHeight;
702
703         this._context.save();
704         this._context.translate(0.5, 0.5);
705         this._context.beginPath();
706         this._context.moveTo(boxX, boxY);
707         if (renderArrowUp) {
708             this._context.lineTo(boxX + 2 * arrowHeight, boxY);
709             this._context.lineTo(boxX + 3 * arrowHeight, boxY - arrowHeight);
710             this._context.lineTo(boxX + 4 * arrowHeight, boxY);
711         }
712         this._context.lineTo(boxX + titleWidth, boxY);
713         this._context.lineTo(boxX + titleWidth, boxY + titleHeight);
714         if (renderArrowDown) {
715             this._context.lineTo(boxX + 4 * arrowHeight, boxY + titleHeight);
716             this._context.lineTo(boxX + 3 * arrowHeight, boxY + titleHeight + arrowHeight);
717             this._context.lineTo(boxX + 2 * arrowHeight, boxY + titleHeight);
718         }
719         this._context.lineTo(boxX, boxY + titleHeight);
720         this._context.closePath();
721         this._context.fillStyle = "rgb(255, 255, 194)";
722         this._context.fill();
723         this._context.strokeStyle = "rgb(128, 128, 128)";
724         this._context.stroke();
725
726         this._context.restore();
727
728         this._titleElement.classList.remove("hidden");
729         this._titleElement.style.top = (boxY + 3) + "px";
730         this._titleElement.style.left = (boxX + 3) + "px";
731     },
732
733     /**
734      * @return {!{width: number, height: number}}
735      */
736     _viewportDimensions: function()
737     {
738         const gutterSize = 30;
739         const bordersSize = WebInspector.ScreencastView._bordersSize;
740         return { width: this.element.offsetWidth - bordersSize - gutterSize,
741                  height: this.element.offsetHeight - bordersSize - gutterSize - WebInspector.ScreencastView._navBarHeight};
742     },
743
744     /**
745      * @param {boolean} enabled
746      * @param {boolean} inspectShadowDOM
747      * @param {!DOMAgent.HighlightConfig} config
748      * @param {function(?Protocol.Error)=} callback
749      */
750     setInspectModeEnabled: function(enabled, inspectShadowDOM, config, callback)
751     {
752         this._inspectModeConfig = enabled ? config : null;
753         if (callback)
754             callback(null);
755     },
756
757     /**
758      * @param {!CanvasRenderingContext2D} context
759      */
760     _createCheckerboardPattern: function(context)
761     {
762         var pattern = /** @type {!HTMLCanvasElement} */(document.createElement("canvas"));
763         const size = 32;
764         pattern.width = size * 2;
765         pattern.height = size * 2;
766         var pctx = pattern.getContext("2d");
767
768         pctx.fillStyle = "rgb(195, 195, 195)";
769         pctx.fillRect(0, 0, size * 2, size * 2);
770
771         pctx.fillStyle = "rgb(225, 225, 225)";
772         pctx.fillRect(0, 0, size, size);
773         pctx.fillRect(size, size, size, size);
774         return context.createPattern(pattern, "repeat");
775     },
776
777     _createNavigationBar: function()
778     {
779         this._navigationBar = this.element.createChild("div", "toolbar-background screencast-navigation");
780
781         this._navigationBack = this._navigationBar.createChild("button", "back");
782         this._navigationBack.disabled = true;
783         this._navigationBack.addEventListener("click", this._navigateToHistoryEntry.bind(this, -1), false);
784
785         this._navigationForward = this._navigationBar.createChild("button", "forward");
786         this._navigationForward.disabled = true;
787         this._navigationForward.addEventListener("click", this._navigateToHistoryEntry.bind(this, 1), false);
788
789         this._navigationReload = this._navigationBar.createChild("button", "reload");
790         this._navigationReload.addEventListener("click", this._navigateReload.bind(this), false);
791
792         this._navigationUrl = this._navigationBar.createChild("input");
793         this._navigationUrl.type = "text";
794         this._navigationUrl.addEventListener('keyup', this._navigationUrlKeyUp.bind(this), true);
795
796         this._navigationProgressBar = new WebInspector.ScreencastView.ProgressTracker(this._navigationBar.createChild("div", "progress"));
797
798         this._requestNavigationHistory();
799         WebInspector.resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.InspectedURLChanged, this._requestNavigationHistory, this);
800     },
801
802     _navigateToHistoryEntry: function(offset)
803     {
804         var newIndex = this._historyIndex + offset;
805         if (newIndex < 0 || newIndex >= this._historyEntries.length)
806           return;
807         PageAgent.navigateToHistoryEntry(this._historyEntries[newIndex].id);
808         this._requestNavigationHistory();
809     },
810
811     _navigateReload: function()
812     {
813         WebInspector.resourceTreeModel.reloadPage();
814     },
815
816     _navigationUrlKeyUp: function(event)
817     {
818         if (event.keyIdentifier != 'Enter')
819             return;
820         var url = this._navigationUrl.value;
821         if (!url)
822             return;
823         if (!url.match(WebInspector.ScreencastView._HttpRegex))
824             url = "http://" + url;
825         PageAgent.navigate(url);
826         this._canvasElement.focus();
827     },
828
829     _requestNavigationHistory: function()
830     {
831         PageAgent.getNavigationHistory(this._onNavigationHistory.bind(this));
832     },
833
834     _onNavigationHistory: function(error, currentIndex, entries)
835     {
836         if (error)
837           return;
838
839         this._historyIndex = currentIndex;
840         this._historyEntries = entries;
841
842         this._navigationBack.disabled = currentIndex == 0;
843         this._navigationForward.disabled = currentIndex == (entries.length - 1);
844
845         var url = entries[currentIndex].url;
846         var match = url.match(WebInspector.ScreencastView._HttpRegex);
847         if (match)
848             url = match[1];
849         this._navigationUrl.value = url;
850     },
851
852     _focusNavigationBar: function()
853     {
854         this._navigationUrl.focus();
855         this._navigationUrl.select();
856         return true;
857     },
858
859   __proto__: WebInspector.View.prototype
860 }
861
862 /**
863  * @param {!HTMLElement} element
864  * @constructor
865  */
866 WebInspector.ScreencastView.ProgressTracker = function(element) {
867     this._element = element;
868
869     WebInspector.resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.MainFrameNavigated, this._onMainFrameNavigated, this);
870     WebInspector.resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.Load, this._onLoad, this);
871
872     WebInspector.networkManager.addEventListener(WebInspector.NetworkManager.EventTypes.RequestStarted, this._onRequestStarted, this);
873     WebInspector.networkManager.addEventListener(WebInspector.NetworkManager.EventTypes.RequestFinished, this._onRequestFinished, this);
874 };
875
876 WebInspector.ScreencastView.ProgressTracker.prototype = {
877     _onMainFrameNavigated: function()
878     {
879         this._requestIds = {};
880         this._startedRequests = 0;
881         this._finishedRequests = 0;
882         this._maxDisplayedProgress = 0;
883         this._updateProgress(0.1);  // Display first 10% on navigation start.
884     },
885
886     _onLoad: function()
887     {
888         delete this._requestIds;
889         this._updateProgress(1);  // Display 100% progress on load, hide it in 0.5s.
890         setTimeout(function() {
891             if (!this._navigationProgressVisible())
892                 this._displayProgress(0);
893         }.bind(this), 500);
894     },
895
896     _navigationProgressVisible: function()
897     {
898         return !!this._requestIds;
899     },
900
901     _onRequestStarted: function(event)
902     {
903       if (!this._navigationProgressVisible())
904           return;
905       var request = /** @type {!WebInspector.NetworkRequest} */ (event.data);
906       // Ignore long-living WebSockets for the sake of progress indicator, as we won't be waiting them anyway.
907       if (request.type === WebInspector.resourceTypes.WebSocket)
908           return;
909       this._requestIds[request.requestId] = request;
910       ++this._startedRequests;
911     },
912
913     _onRequestFinished: function(event)
914     {
915         if (!this._navigationProgressVisible())
916             return;
917         var request = /** @type {!WebInspector.NetworkRequest} */ (event.data);
918         if (!(request.requestId in this._requestIds))
919             return;
920         ++this._finishedRequests;
921         setTimeout(function() {
922             this._updateProgress(this._finishedRequests / this._startedRequests * 0.9);  // Finished requests drive the progress up to 90%.
923         }.bind(this), 500);  // Delay to give the new requests time to start. This makes the progress smoother.
924     },
925
926     _updateProgress: function(progress)
927     {
928         if (!this._navigationProgressVisible())
929           return;
930         if (this._maxDisplayedProgress >= progress)
931           return;
932         this._maxDisplayedProgress = progress;
933         this._displayProgress(progress);
934     },
935
936     _displayProgress: function(progress)
937     {
938         this._element.style.width = (100 * progress) + "%";
939     }
940 };