Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / third_party / WebKit / Source / devtools / front_end / screencast / 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.VBox}
34  * @implements {WebInspector.DOMNodeHighlighter}
35  * @param {!WebInspector.Target} target
36  */
37 WebInspector.ScreencastView = function(target)
38 {
39     WebInspector.VBox.call(this);
40     this._target = target;
41
42     this.setMinimumSize(150, 150);
43     this.registerRequiredCSS("screencastView.css");
44 };
45
46 WebInspector.ScreencastView._bordersSize = 44;
47
48 WebInspector.ScreencastView._navBarHeight = 29;
49
50 WebInspector.ScreencastView._HttpRegex = /^https?:\/\/(.+)/;
51
52 WebInspector.ScreencastView.prototype = {
53     initialize: function()
54     {
55         this.element.classList.add("screencast");
56
57         this._createNavigationBar();
58
59         this._viewportElement = this.element.createChild("div", "screencast-viewport hidden");
60         this._canvasContainerElement = this._viewportElement.createChild("div", "screencast-canvas-container");
61         this._glassPaneElement = this._canvasContainerElement.createChild("div", "screencast-glasspane hidden");
62
63         this._canvasElement = this._canvasContainerElement.createChild("canvas");
64         this._canvasElement.tabIndex = 1;
65         this._canvasElement.addEventListener("mousedown", this._handleMouseEvent.bind(this), false);
66         this._canvasElement.addEventListener("mouseup", this._handleMouseEvent.bind(this), false);
67         this._canvasElement.addEventListener("mousemove", this._handleMouseEvent.bind(this), false);
68         this._canvasElement.addEventListener("mousewheel", this._handleMouseEvent.bind(this), false);
69         this._canvasElement.addEventListener("click", this._handleMouseEvent.bind(this), false);
70         this._canvasElement.addEventListener("contextmenu", this._handleContextMenuEvent.bind(this), false);
71         this._canvasElement.addEventListener("keydown", this._handleKeyEvent.bind(this), false);
72         this._canvasElement.addEventListener("keyup", this._handleKeyEvent.bind(this), false);
73         this._canvasElement.addEventListener("keypress", this._handleKeyEvent.bind(this), false);
74         this._canvasElement.addEventListener("blur", this._handleBlurEvent.bind(this), false);
75
76         this._titleElement = this._canvasContainerElement.createChild("div", "screencast-element-title monospace hidden");
77         this._tagNameElement = this._titleElement.createChild("span", "screencast-tag-name");
78         this._nodeIdElement = this._titleElement.createChild("span", "screencast-node-id");
79         this._classNameElement = this._titleElement.createChild("span", "screencast-class-name");
80         this._titleElement.createTextChild(" ");
81         this._nodeWidthElement = this._titleElement.createChild("span");
82         this._titleElement.createChild("span", "screencast-px").textContent = "px";
83         this._titleElement.createTextChild(" \u00D7 ");
84         this._nodeHeightElement = this._titleElement.createChild("span");
85         this._titleElement.createChild("span", "screencast-px").textContent = "px";
86
87         this._imageElement = new Image();
88         this._isCasting = false;
89         this._context = this._canvasElement.getContext("2d");
90         this._checkerboardPattern = this._createCheckerboardPattern(this._context);
91
92         this._shortcuts = /** !Object.<number, function(Event=):boolean> */ ({});
93         this._shortcuts[WebInspector.KeyboardShortcut.makeKey("l", WebInspector.KeyboardShortcut.Modifiers.Ctrl)] = this._focusNavigationBar.bind(this);
94
95         WebInspector.resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.ScreencastFrame, this._screencastFrame, this);
96         WebInspector.resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.ScreencastVisibilityChanged, this._screencastVisibilityChanged, this);
97
98         WebInspector.profilingLock().addEventListener(WebInspector.Lock.Events.StateChanged, this._onProfilingStateChange, this);
99         this._updateGlasspane();
100     },
101
102     wasShown: function()
103     {
104         this._startCasting();
105     },
106
107     willHide: function()
108     {
109         this._stopCasting();
110     },
111
112     _startCasting: function()
113     {
114         if (WebInspector.profilingLock().isAcquired())
115             return;
116         if (this._isCasting)
117             return;
118         this._isCasting = true;
119
120         const maxImageDimension = 2048;
121         var dimensions = this._viewportDimensions();
122         if (dimensions.width < 0 || dimensions.height < 0) {
123             this._isCasting = false;
124             return;
125         }
126         dimensions.width *= window.devicePixelRatio;
127         dimensions.height *= window.devicePixelRatio;
128         this._target.pageAgent().startScreencast("jpeg", 80, Math.min(maxImageDimension, dimensions.width), Math.min(maxImageDimension, dimensions.height));
129         this._target.domModel.setHighlighter(this);
130     },
131
132     _stopCasting: function()
133     {
134         if (!this._isCasting)
135             return;
136         this._isCasting = false;
137         this._target.pageAgent().stopScreencast();
138         this._target.domModel.setHighlighter(null);
139     },
140
141     /**
142      * @param {!WebInspector.Event} event
143      */
144     _screencastFrame: function(event)
145     {
146         var metadata = /** type {PageAgent.ScreencastFrameMetadata} */(event.data.metadata);
147         var base64Data = /** type {string} */(event.data.data);
148         this._imageElement.src = "data:image/jpg;base64," + base64Data;
149         this._pageScaleFactor = metadata.pageScaleFactor;
150         this._screenOffsetTop = metadata.offsetTop;
151         this._deviceWidth = metadata.deviceWidth;
152         this._deviceHeight = metadata.deviceHeight;
153         this._scrollOffsetX = metadata.scrollOffsetX;
154         this._scrollOffsetY = metadata.scrollOffsetY;
155
156         var deviceSizeRatio = metadata.deviceHeight / metadata.deviceWidth;
157         var dimensionsCSS = this._viewportDimensions();
158
159         this._imageZoom = Math.min(dimensionsCSS.width / this._imageElement.naturalWidth, dimensionsCSS.height / (this._imageElement.naturalWidth * deviceSizeRatio));
160         this._viewportElement.classList.remove("hidden");
161         var bordersSize = WebInspector.ScreencastView._bordersSize;
162         if (this._imageZoom < 1.01 / window.devicePixelRatio)
163             this._imageZoom = 1 / window.devicePixelRatio;
164         this._screenZoom = this._imageElement.naturalWidth * this._imageZoom / metadata.deviceWidth;
165         this._viewportElement.style.width = metadata.deviceWidth * this._screenZoom + bordersSize + "px";
166         this._viewportElement.style.height = metadata.deviceHeight * this._screenZoom + bordersSize + "px";
167
168         this.highlightDOMNode(this._highlightNode, this._highlightConfig);
169     },
170
171     _isGlassPaneActive: function()
172     {
173         return !this._glassPaneElement.classList.contains("hidden");
174     },
175
176     /**
177      * @param {!WebInspector.Event} event
178      */
179     _screencastVisibilityChanged: function(event)
180     {
181         this._targetInactive = !event.data.visible;
182         this._updateGlasspane();
183     },
184
185     /**
186      * @param {!WebInspector.Event} event
187      */
188     _onProfilingStateChange: function(event)
189     {
190         if (WebInspector.profilingLock().isAcquired())
191             this._stopCasting();
192         else
193             this._startCasting();
194         this._updateGlasspane();
195     },
196
197     _updateGlasspane: function()
198     {
199         if (this._targetInactive) {
200             this._glassPaneElement.textContent = WebInspector.UIString("The tab is inactive");
201             this._glassPaneElement.classList.remove("hidden");
202         } else if (WebInspector.profilingLock().isAcquired()) {
203             this._glassPaneElement.textContent = WebInspector.UIString("Profiling in progress");
204             this._glassPaneElement.classList.remove("hidden");
205         } else {
206             this._glassPaneElement.classList.add("hidden");
207         }
208     },
209
210     /**
211      * @param {!Event} event
212      */
213     _handleMouseEvent: function(event)
214     {
215         if (this._isGlassPaneActive()) {
216           event.consume();
217           return;
218         }
219
220         if (!this._pageScaleFactor)
221             return;
222
223         if (!this._inspectModeConfig || event.type === "mousewheel") {
224             this._simulateTouchForMouseEvent(event);
225             event.preventDefault();
226             if (event.type === "mousedown")
227                 this._canvasElement.focus();
228             return;
229         }
230
231         var position = this._convertIntoScreenSpace(event);
232         this._target.domModel.nodeForLocation(position.x / this._pageScaleFactor + this._scrollOffsetX, position.y / this._pageScaleFactor + this._scrollOffsetY, callback.bind(this));
233
234         /**
235          * @param {?WebInspector.DOMNode} node
236          * @this {WebInspector.ScreencastView}
237          */
238         function callback(node)
239         {
240             if (!node)
241                 return;
242             if (event.type === "mousemove")
243                 this.highlightDOMNode(node, this._inspectModeConfig);
244             else if (event.type === "click")
245                 WebInspector.Revealer.reveal(node);
246         }
247     },
248
249     /**
250      * @param {!Event} event
251      */
252     _handleKeyEvent: function(event)
253     {
254         if (this._isGlassPaneActive()) {
255             event.consume();
256             return;
257         }
258
259         var shortcutKey = WebInspector.KeyboardShortcut.makeKeyFromEvent(/** @type {!KeyboardEvent} */ (event));
260         var handler = this._shortcuts[shortcutKey];
261         if (handler && handler(event)) {
262             event.consume();
263             return;
264         }
265
266         var type;
267         switch (event.type) {
268         case "keydown": type = "keyDown"; break;
269         case "keyup": type = "keyUp"; break;
270         case "keypress": type = "char"; break;
271         default: return;
272         }
273
274         var text = event.type === "keypress" ? String.fromCharCode(event.charCode) : undefined;
275         InputAgent.dispatchKeyEvent(type, this._modifiersForEvent(event), event.timeStamp / 1000, text, text ? text.toLowerCase() : undefined,
276                                     event.keyIdentifier, event.keyCode /* windowsVirtualKeyCode */, event.keyCode /* nativeVirtualKeyCode */, false, false, false);
277         event.consume();
278         this._canvasElement.focus();
279     },
280
281     /**
282      * @param {!Event} event
283      */
284     _handleContextMenuEvent: function(event)
285     {
286         event.consume(true);
287     },
288
289     /**
290      * @param {!Event} event
291      */
292     _simulateTouchForMouseEvent: function(event)
293     {
294         const buttons = {0: "none", 1: "left", 2: "middle", 3: "right"};
295         const types = {"mousedown" : "mousePressed", "mouseup": "mouseReleased", "mousemove": "mouseMoved", "mousewheel": "mouseWheel"};
296         if (!(event.type in types) || !(event.which in buttons))
297             return;
298         if (event.type !== "mousewheel" && buttons[event.which] === "none")
299             return;
300
301         if (event.type === "mousedown" || typeof this._eventScreenOffsetTop === "undefined")
302             this._eventScreenOffsetTop = this._screenOffsetTop;
303
304         var modifiers = (event.altKey ? 1 : 0) | (event.ctrlKey ? 2 : 0) | (event.metaKey ? 4 : 0) | (event.shiftKey ? 8 : 0);
305
306         var convertedPosition = this._zoomIntoScreenSpace(event);
307         convertedPosition.y = Math.round(convertedPosition.y - this._eventScreenOffsetTop);
308         var params = {type: types[event.type], x: convertedPosition.x, y: convertedPosition.y, modifiers: modifiers, timestamp: event.timeStamp / 1000, button: buttons[event.which], clickCount: 0};
309         if (event.type === "mousewheel") {
310             params.deltaX = event.wheelDeltaX / this._screenZoom;
311             params.deltaY = event.wheelDeltaY / this._screenZoom;
312         } else {
313             this._eventParams = params;
314         }
315         if (event.type === "mouseup")
316             delete this._eventScreenOffsetTop;
317         InputAgent.invoke_emulateTouchFromMouseEvent(params);
318     },
319
320     /**
321      * @param {!Event} event
322      */
323     _handleBlurEvent: function(event)
324     {
325         if (typeof this._eventScreenOffsetTop !== "undefined") {
326             var params = this._eventParams;
327             delete this._eventParams;
328             params.type = "mouseReleased";
329             InputAgent.invoke_emulateTouchFromMouseEvent(params);
330         }
331     },
332
333     /**
334      * @param {!Event} event
335      * @return {!{x: number, y: number}}
336      */
337     _zoomIntoScreenSpace: function(event)
338     {
339         var position  = {};
340         position.x = Math.round(event.offsetX / this._screenZoom);
341         position.y = Math.round(event.offsetY / this._screenZoom);
342         return position;
343     },
344
345     /**
346      * @param {!Event} event
347      * @return {!{x: number, y: number}}
348      */
349     _convertIntoScreenSpace: function(event)
350     {
351         var position = this._zoomIntoScreenSpace(event);
352         position.y = Math.round(position.y - this._screenOffsetTop);
353         return position;
354     },
355
356     /**
357      * @param {!Event} event
358      * @return {number}
359      */
360     _modifiersForEvent: function(event)
361     {
362         var modifiers = 0;
363         if (event.altKey)
364             modifiers = 1;
365         if (event.ctrlKey)
366             modifiers += 2;
367         if (event.metaKey)
368             modifiers += 4;
369         if (event.shiftKey)
370             modifiers += 8;
371         return modifiers;
372     },
373
374     onResize: function()
375     {
376         if (this._deferredCasting) {
377             clearTimeout(this._deferredCasting);
378             delete this._deferredCasting;
379         }
380
381         this._stopCasting();
382         this._deferredCasting = setTimeout(this._startCasting.bind(this), 100);
383     },
384
385     /**
386      * @param {?WebInspector.DOMNode} node
387      * @param {?DOMAgent.HighlightConfig} config
388      * @param {!RuntimeAgent.RemoteObjectId=} objectId
389      */
390     highlightDOMNode: function(node, config, objectId)
391     {
392         this._highlightNode = node;
393         this._highlightConfig = config;
394         if (!node) {
395             this._model = null;
396             this._config = null;
397             this._node = null;
398             this._titleElement.classList.add("hidden");
399             this._repaint();
400             return;
401         }
402
403         this._node = node;
404         node.boxModel(callback.bind(this));
405
406         /**
407          * @param {?DOMAgent.BoxModel} model
408          * @this {WebInspector.ScreencastView}
409          */
410         function callback(model)
411         {
412             if (!model || !this._pageScaleFactor) {
413                 this._repaint();
414                 return;
415             }
416             this._model = this._scaleModel(model);
417             this._config = config;
418             this._repaint();
419         }
420     },
421
422     /**
423      * @param {!DOMAgent.BoxModel} model
424      * @return {!DOMAgent.BoxModel}
425      */
426     _scaleModel: function(model)
427     {
428         /**
429          * @param {!DOMAgent.Quad} quad
430          * @this {WebInspector.ScreencastView}
431          */
432         function scaleQuad(quad)
433         {
434             for (var i = 0; i < quad.length; i += 2) {
435                 quad[i] = quad[i] * this._pageScaleFactor * this._screenZoom;
436                 quad[i + 1] = (quad[i + 1] * this._pageScaleFactor + this._screenOffsetTop) * this._screenZoom;
437             }
438         }
439
440         scaleQuad.call(this, model.content);
441         scaleQuad.call(this, model.padding);
442         scaleQuad.call(this, model.border);
443         scaleQuad.call(this, model.margin);
444         return model;
445     },
446
447     _repaint: function()
448     {
449         var model = this._model;
450         var config = this._config;
451
452         var canvasWidth = this._canvasElement.getBoundingClientRect().width;
453         var canvasHeight = this._canvasElement.getBoundingClientRect().height;
454         this._canvasElement.width = window.devicePixelRatio * canvasWidth;
455         this._canvasElement.height = window.devicePixelRatio * canvasHeight;
456
457         this._context.save();
458         this._context.scale(window.devicePixelRatio, window.devicePixelRatio);
459
460         // Paint top and bottom gutter.
461         this._context.save();
462         this._context.fillStyle = this._checkerboardPattern;
463         this._context.fillRect(0, 0, canvasWidth, this._screenOffsetTop * this._screenZoom);
464         this._context.fillRect(0, this._screenOffsetTop * this._screenZoom + this._imageElement.naturalHeight * this._imageZoom, canvasWidth, canvasHeight);
465         this._context.restore();
466
467         if (model && config) {
468             this._context.save();
469             const transparentColor = "rgba(0, 0, 0, 0)";
470             var hasContent = model.content && config.contentColor !== transparentColor;
471             var hasPadding = model.padding && config.paddingColor !== transparentColor;
472             var hasBorder = model.border && config.borderColor !== transparentColor;
473             var hasMargin = model.margin && config.marginColor !== transparentColor;
474
475             var clipQuad;
476             if (hasMargin && (!hasBorder || !this._quadsAreEqual(model.margin, model.border))) {
477                 this._drawOutlinedQuadWithClip(model.margin, model.border, config.marginColor);
478                 clipQuad = model.border;
479             }
480             if (hasBorder && (!hasPadding || !this._quadsAreEqual(model.border, model.padding))) {
481                 this._drawOutlinedQuadWithClip(model.border, model.padding, config.borderColor);
482                 clipQuad = model.padding;
483             }
484             if (hasPadding && (!hasContent || !this._quadsAreEqual(model.padding, model.content))) {
485                 this._drawOutlinedQuadWithClip(model.padding, model.content, config.paddingColor);
486                 clipQuad = model.content;
487             }
488             if (hasContent)
489                 this._drawOutlinedQuad(model.content, config.contentColor);
490             this._context.restore();
491
492             this._drawElementTitle();
493
494             this._context.globalCompositeOperation = "destination-over";
495         }
496
497         this._context.drawImage(this._imageElement, 0, this._screenOffsetTop * this._screenZoom, this._imageElement.naturalWidth * this._imageZoom, this._imageElement.naturalHeight * this._imageZoom);
498         this._context.restore();
499
500     },
501
502
503     /**
504      * @param {!DOMAgent.Quad} quad1
505      * @param {!DOMAgent.Quad} quad2
506      * @return {boolean}
507      */
508     _quadsAreEqual: function(quad1, quad2)
509     {
510         for (var i = 0; i < quad1.length; ++i) {
511             if (quad1[i] !== quad2[i])
512                 return false;
513         }
514         return true;
515     },
516
517     /**
518      * @param {!DOMAgent.RGBA} color
519      * @return {string}
520      */
521     _cssColor: function(color)
522     {
523         if (!color)
524             return "transparent";
525         return WebInspector.Color.fromRGBA([color.r, color.g, color.b, color.a]).toString(WebInspector.Color.Format.RGBA) || "";
526     },
527
528     /**
529      * @param {!DOMAgent.Quad} quad
530      * @return {!CanvasRenderingContext2D}
531      */
532     _quadToPath: function(quad)
533     {
534         this._context.beginPath();
535         this._context.moveTo(quad[0], quad[1]);
536         this._context.lineTo(quad[2], quad[3]);
537         this._context.lineTo(quad[4], quad[5]);
538         this._context.lineTo(quad[6], quad[7]);
539         this._context.closePath();
540         return this._context;
541     },
542
543     /**
544      * @param {!DOMAgent.Quad} quad
545      * @param {!DOMAgent.RGBA} fillColor
546      */
547     _drawOutlinedQuad: function(quad, fillColor)
548     {
549         this._context.save();
550         this._context.lineWidth = 2;
551         this._quadToPath(quad).clip();
552         this._context.fillStyle = this._cssColor(fillColor);
553         this._context.fill();
554         this._context.restore();
555     },
556
557     /**
558      * @param {!DOMAgent.Quad} quad
559      * @param {!DOMAgent.Quad} clipQuad
560      * @param {!DOMAgent.RGBA} fillColor
561      */
562     _drawOutlinedQuadWithClip: function (quad, clipQuad, fillColor)
563     {
564         this._context.fillStyle = this._cssColor(fillColor);
565         this._context.save();
566         this._context.lineWidth = 0;
567         this._quadToPath(quad).fill();
568         this._context.globalCompositeOperation = "destination-out";
569         this._context.fillStyle = "red";
570         this._quadToPath(clipQuad).fill();
571         this._context.restore();
572     },
573
574     _drawElementTitle: function()
575     {
576         if (!this._node)
577             return;
578
579         var canvasWidth = this._canvasElement.getBoundingClientRect().width;
580         var canvasHeight = this._canvasElement.getBoundingClientRect().height;
581
582         var lowerCaseName = this._node.localName() || this._node.nodeName().toLowerCase();
583         this._tagNameElement.textContent = lowerCaseName;
584         this._nodeIdElement.textContent = this._node.getAttribute("id") ? "#" + this._node.getAttribute("id") : "";
585         this._nodeIdElement.textContent = this._node.getAttribute("id") ? "#" + this._node.getAttribute("id") : "";
586         var className = this._node.getAttribute("class");
587         if (className && className.length > 50)
588            className = className.substring(0, 50) + "\u2026";
589         this._classNameElement.textContent = className || "";
590         this._nodeWidthElement.textContent = this._model.width;
591         this._nodeHeightElement.textContent = this._model.height;
592
593         var marginQuad = this._model.margin;
594         var titleWidth = this._titleElement.offsetWidth + 6;
595         var titleHeight = this._titleElement.offsetHeight + 4;
596
597         var anchorTop = this._model.margin[1];
598         var anchorBottom = this._model.margin[7];
599
600         const arrowHeight = 7;
601         var renderArrowUp = false;
602         var renderArrowDown = false;
603
604         var boxX = Math.max(2, this._model.margin[0]);
605         if (boxX + titleWidth > canvasWidth)
606             boxX = canvasWidth - titleWidth - 2;
607
608         var boxY;
609         if (anchorTop > canvasHeight) {
610             boxY = canvasHeight - titleHeight - arrowHeight;
611             renderArrowDown = true;
612         } else if (anchorBottom < 0) {
613             boxY = arrowHeight;
614             renderArrowUp = true;
615         } else if (anchorBottom + titleHeight + arrowHeight < canvasHeight) {
616             boxY = anchorBottom + arrowHeight - 4;
617             renderArrowUp = true;
618         } else if (anchorTop - titleHeight - arrowHeight > 0) {
619             boxY = anchorTop - titleHeight - arrowHeight + 3;
620             renderArrowDown = true;
621         } else
622             boxY = arrowHeight;
623
624         this._context.save();
625         this._context.translate(0.5, 0.5);
626         this._context.beginPath();
627         this._context.moveTo(boxX, boxY);
628         if (renderArrowUp) {
629             this._context.lineTo(boxX + 2 * arrowHeight, boxY);
630             this._context.lineTo(boxX + 3 * arrowHeight, boxY - arrowHeight);
631             this._context.lineTo(boxX + 4 * arrowHeight, boxY);
632         }
633         this._context.lineTo(boxX + titleWidth, boxY);
634         this._context.lineTo(boxX + titleWidth, boxY + titleHeight);
635         if (renderArrowDown) {
636             this._context.lineTo(boxX + 4 * arrowHeight, boxY + titleHeight);
637             this._context.lineTo(boxX + 3 * arrowHeight, boxY + titleHeight + arrowHeight);
638             this._context.lineTo(boxX + 2 * arrowHeight, boxY + titleHeight);
639         }
640         this._context.lineTo(boxX, boxY + titleHeight);
641         this._context.closePath();
642         this._context.fillStyle = "rgb(255, 255, 194)";
643         this._context.fill();
644         this._context.strokeStyle = "rgb(128, 128, 128)";
645         this._context.stroke();
646
647         this._context.restore();
648
649         this._titleElement.classList.remove("hidden");
650         this._titleElement.style.top = (boxY + 3) + "px";
651         this._titleElement.style.left = (boxX + 3) + "px";
652     },
653
654     /**
655      * @return {!{width: number, height: number}}
656      */
657     _viewportDimensions: function()
658     {
659         const gutterSize = 30;
660         const bordersSize = WebInspector.ScreencastView._bordersSize;
661         var width = this.element.offsetWidth - bordersSize - gutterSize;
662         var height = this.element.offsetHeight - bordersSize - gutterSize - WebInspector.ScreencastView._navBarHeight;
663         return { width: width, height: height };
664     },
665
666     /**
667      * @param {boolean} enabled
668      * @param {boolean} inspectUAShadowDOM
669      * @param {!DOMAgent.HighlightConfig} config
670      * @param {function(?Protocol.Error)=} callback
671      */
672     setInspectModeEnabled: function(enabled, inspectUAShadowDOM, config, callback)
673     {
674         this._inspectModeConfig = enabled ? config : null;
675         if (callback)
676             callback(null);
677     },
678
679     /**
680      * @param {!CanvasRenderingContext2D} context
681      */
682     _createCheckerboardPattern: function(context)
683     {
684         var pattern = /** @type {!HTMLCanvasElement} */(document.createElement("canvas"));
685         const size = 32;
686         pattern.width = size * 2;
687         pattern.height = size * 2;
688         var pctx = pattern.getContext("2d");
689
690         pctx.fillStyle = "rgb(195, 195, 195)";
691         pctx.fillRect(0, 0, size * 2, size * 2);
692
693         pctx.fillStyle = "rgb(225, 225, 225)";
694         pctx.fillRect(0, 0, size, size);
695         pctx.fillRect(size, size, size, size);
696         return context.createPattern(pattern, "repeat");
697     },
698
699     _createNavigationBar: function()
700     {
701         this._navigationBar = this.element.createChild("div", "toolbar-background toolbar-colors screencast-navigation");
702         if (Runtime.queryParam("hideNavigation"))
703             this._navigationBar.classList.add("hidden");
704
705         this._navigationBack = this._navigationBar.createChild("button", "back");
706         this._navigationBack.disabled = true;
707         this._navigationBack.addEventListener("click", this._navigateToHistoryEntry.bind(this, -1), false);
708
709         this._navigationForward = this._navigationBar.createChild("button", "forward");
710         this._navigationForward.disabled = true;
711         this._navigationForward.addEventListener("click", this._navigateToHistoryEntry.bind(this, 1), false);
712
713         this._navigationReload = this._navigationBar.createChild("button", "reload");
714         this._navigationReload.addEventListener("click", this._navigateReload.bind(this), false);
715
716         this._navigationUrl = this._navigationBar.createChild("input");
717         this._navigationUrl.type = "text";
718         this._navigationUrl.addEventListener('keyup', this._navigationUrlKeyUp.bind(this), true);
719
720         this._navigationProgressBar = new WebInspector.ScreencastView.ProgressTracker(this._navigationBar.createChild("div", "progress"));
721
722         this._requestNavigationHistory();
723         WebInspector.targetManager.addEventListener(WebInspector.TargetManager.Events.InspectedURLChanged, this._requestNavigationHistory, this);
724     },
725
726     _navigateToHistoryEntry: function(offset)
727     {
728         var newIndex = this._historyIndex + offset;
729         if (newIndex < 0 || newIndex >= this._historyEntries.length)
730           return;
731         PageAgent.navigateToHistoryEntry(this._historyEntries[newIndex].id);
732         this._requestNavigationHistory();
733     },
734
735     _navigateReload: function()
736     {
737         WebInspector.resourceTreeModel.reloadPage();
738     },
739
740     _navigationUrlKeyUp: function(event)
741     {
742         if (event.keyIdentifier != 'Enter')
743             return;
744         var url = this._navigationUrl.value;
745         if (!url)
746             return;
747         if (!url.match(WebInspector.ScreencastView._HttpRegex))
748             url = "http://" + url;
749         PageAgent.navigate(url);
750         this._canvasElement.focus();
751     },
752
753     _requestNavigationHistory: function()
754     {
755         PageAgent.getNavigationHistory(this._onNavigationHistory.bind(this));
756     },
757
758     _onNavigationHistory: function(error, currentIndex, entries)
759     {
760         if (error)
761           return;
762
763         this._historyIndex = currentIndex;
764         this._historyEntries = entries;
765
766         this._navigationBack.disabled = currentIndex == 0;
767         this._navigationForward.disabled = currentIndex == (entries.length - 1);
768
769         var url = entries[currentIndex].url;
770         var match = url.match(WebInspector.ScreencastView._HttpRegex);
771         if (match)
772             url = match[1];
773         InspectorFrontendHost.inspectedURLChanged(url);
774         this._navigationUrl.value = url;
775     },
776
777     _focusNavigationBar: function()
778     {
779         this._navigationUrl.focus();
780         this._navigationUrl.select();
781         return true;
782     },
783
784   __proto__: WebInspector.VBox.prototype
785 }
786
787 /**
788  * @param {!Element} element
789  * @constructor
790  */
791 WebInspector.ScreencastView.ProgressTracker = function(element)
792 {
793     this._element = element;
794
795     WebInspector.targetManager.addModelListener(WebInspector.ResourceTreeModel, WebInspector.ResourceTreeModel.EventTypes.MainFrameNavigated, this._onMainFrameNavigated, this);
796     WebInspector.targetManager.addModelListener(WebInspector.ResourceTreeModel, WebInspector.ResourceTreeModel.EventTypes.Load, this._onLoad, this);
797     WebInspector.targetManager.addModelListener(WebInspector.NetworkManager, WebInspector.NetworkManager.EventTypes.RequestStarted, this._onRequestStarted, this);
798     WebInspector.targetManager.addModelListener(WebInspector.NetworkManager, WebInspector.NetworkManager.EventTypes.RequestFinished, this._onRequestFinished, this);
799 }
800
801 WebInspector.ScreencastView.ProgressTracker.prototype = {
802     _onMainFrameNavigated: function()
803     {
804         this._requestIds = {};
805         this._startedRequests = 0;
806         this._finishedRequests = 0;
807         this._maxDisplayedProgress = 0;
808         this._updateProgress(0.1);  // Display first 10% on navigation start.
809     },
810
811     _onLoad: function()
812     {
813         delete this._requestIds;
814         this._updateProgress(1);  // Display 100% progress on load, hide it in 0.5s.
815         setTimeout(function() {
816             if (!this._navigationProgressVisible())
817                 this._displayProgress(0);
818         }.bind(this), 500);
819     },
820
821     _navigationProgressVisible: function()
822     {
823         return !!this._requestIds;
824     },
825
826     _onRequestStarted: function(event)
827     {
828       if (!this._navigationProgressVisible())
829           return;
830       var request = /** @type {!WebInspector.NetworkRequest} */ (event.data);
831       // Ignore long-living WebSockets for the sake of progress indicator, as we won't be waiting them anyway.
832       if (request.type === WebInspector.resourceTypes.WebSocket)
833           return;
834       this._requestIds[request.requestId] = request;
835       ++this._startedRequests;
836     },
837
838     _onRequestFinished: function(event)
839     {
840         if (!this._navigationProgressVisible())
841             return;
842         var request = /** @type {!WebInspector.NetworkRequest} */ (event.data);
843         if (!(request.requestId in this._requestIds))
844             return;
845         ++this._finishedRequests;
846         setTimeout(function() {
847             this._updateProgress(this._finishedRequests / this._startedRequests * 0.9);  // Finished requests drive the progress up to 90%.
848         }.bind(this), 500);  // Delay to give the new requests time to start. This makes the progress smoother.
849     },
850
851     _updateProgress: function(progress)
852     {
853         if (!this._navigationProgressVisible())
854           return;
855         if (this._maxDisplayedProgress >= progress)
856           return;
857         this._maxDisplayedProgress = progress;
858         this._displayProgress(progress);
859     },
860
861     _displayProgress: function(progress)
862     {
863         this._element.style.width = (100 * progress) + "%";
864     }
865 };