Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(84)

Side by Side Diff: Source/devtools/front_end/Layers3DView.js

Issue 177353005: Layers view: Extract zoom/pan/rotate logic into transorm controller (Closed) Base URL: svn://svn.chromium.org/blink/trunk
Patch Set: added rounding of layer divs positions and sizes, fixed the test Created 6 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « Source/devtools/devtools.gypi ('k') | Source/devtools/front_end/LayersPanel.js » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 /* 1 /*
2 * Copyright (C) 2013 Google Inc. All rights reserved. 2 * Copyright (C) 2014 Google Inc. All rights reserved.
3 * 3 *
4 * Redistribution and use in source and binary forms, with or without 4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are 5 * modification, are permitted provided that the following conditions are
6 * met: 6 * met:
7 * 7 *
8 * * Redistributions of source code must retain the above copyright 8 * * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer. 9 * notice, this list of conditions and the following disclaimer.
10 * * Redistributions in binary form must reproduce the above 10 * * Redistributions in binary form must reproduce the above
11 * copyright notice, this list of conditions and the following disclaimer 11 * copyright notice, this list of conditions and the following disclaimer
12 * in the documentation and/or other materials provided with the 12 * in the documentation and/or other materials provided with the
(...skipping 22 matching lines...) Expand all
35 */ 35 */
36 WebInspector.Layers3DView = function(model) 36 WebInspector.Layers3DView = function(model)
37 { 37 {
38 WebInspector.View.call(this); 38 WebInspector.View.call(this);
39 this.element.classList.add("layers-3d-view"); 39 this.element.classList.add("layers-3d-view");
40 this._emptyView = new WebInspector.EmptyView(WebInspector.UIString("Not in t he composited mode.\nConsider forcing composited mode in Settings.")); 40 this._emptyView = new WebInspector.EmptyView(WebInspector.UIString("Not in t he composited mode.\nConsider forcing composited mode in Settings."));
41 this._model = model; 41 this._model = model;
42 this._model.addEventListener(WebInspector.LayerTreeModel.Events.LayerTreeCha nged, this._update, this); 42 this._model.addEventListener(WebInspector.LayerTreeModel.Events.LayerTreeCha nged, this._update, this);
43 this._model.addEventListener(WebInspector.LayerTreeModel.Events.LayerPainted , this._onLayerPainted, this); 43 this._model.addEventListener(WebInspector.LayerTreeModel.Events.LayerPainted , this._onLayerPainted, this);
44 this._rotatingContainerElement = this.element.createChild("div", "fill rotat ing-container"); 44 this._rotatingContainerElement = this.element.createChild("div", "fill rotat ing-container");
45 this.element.addEventListener("mousemove", this._onMouseMove.bind(this), fal se); 45 this._transformController = new WebInspector.TransformController(this.elemen t);
46 this.element.addEventListener("mouseout", this._onMouseMove.bind(this), fals e); 46 this._transformController.addEventListener(WebInspector.TransformController. Events.TransformChanged, this._onTransformChanged, this);
47 this.element.addEventListener("mousedown", this._onMouseDown.bind(this), fal se);
48 this.element.addEventListener("mouseup", this._onMouseUp.bind(this), false);
49 this.element.addEventListener("contextmenu", this._onContextMenu.bind(this), false);
50 this.element.addEventListener("dblclick", this._onDoubleClick.bind(this), fa lse); 47 this.element.addEventListener("dblclick", this._onDoubleClick.bind(this), fa lse);
51 this.element.addEventListener("click", this._onClick.bind(this), false); 48 this.element.addEventListener("click", this._onClick.bind(this), false);
49 this.element.addEventListener("mouseout", this._onMouseMove.bind(this), fals e);
50 this.element.addEventListener("mousemove", this._onMouseMove.bind(this), fal se);
51 this.element.addEventListener("contextmenu", this._onContextMenu.bind(this), false);
52 this._elementsByLayerId = {}; 52 this._elementsByLayerId = {};
53 this._rotateX = 0;
54 this._rotateY = 0;
55 this._scaleAdjustmentStylesheet = this.element.ownerDocument.head.createChil d("style"); 53 this._scaleAdjustmentStylesheet = this.element.ownerDocument.head.createChil d("style");
56 this._scaleAdjustmentStylesheet.disabled = true; 54 this._scaleAdjustmentStylesheet.disabled = true;
57 this._lastOutlinedElement = {}; 55 this._lastOutlinedElement = {};
58 this._layerImage = document.createElement("img"); 56 this._layerImage = document.createElement("img");
57 this._layerImage.style.width = "100%";
58 this._layerImage.style.height = "100%";
59 WebInspector.settings.showPaintRects.addChangeListener(this._update, this); 59 WebInspector.settings.showPaintRects.addChangeListener(this._update, this);
60 } 60 }
61 61
62 /** 62 /**
63 * @enum {string} 63 * @enum {string}
64 */ 64 */
65 WebInspector.Layers3DView.OutlineType = { 65 WebInspector.Layers3DView.OutlineType = {
66 Hovered: "hovered", 66 Hovered: "hovered",
67 Selected: "selected" 67 Selected: "selected"
68 } 68 }
(...skipping 80 matching lines...) Expand 10 before | Expand all | Expand 10 after
149 if (imageURL) 149 if (imageURL)
150 this._layerImage.src = imageURL; 150 this._layerImage.src = imageURL;
151 element.appendChild(this._layerImage); 151 element.appendChild(this._layerImage);
152 }, 152 },
153 153
154 _scaleToFit: function() 154 _scaleToFit: function()
155 { 155 {
156 var root = this._model.contentRoot(); 156 var root = this._model.contentRoot();
157 if (!root) 157 if (!root)
158 return; 158 return;
159
159 const padding = 40; 160 const padding = 40;
160 var scaleX = this._clientWidth / (root.width() + 2 * padding); 161 var scaleX = this._clientWidth / (root.width() + 2 * padding);
161 var scaleY = this._clientHeight / (root.height() + 2 * padding); 162 var scaleY = this._clientHeight / (root.height() + 2 * padding);
162 this._scale = Math.min(scaleX, scaleY); 163 var autoScale = Math.min(scaleX, scaleY);
163 164
165 this._scale = autoScale * this._transformController.scale();
166 this._paddingX = ((this._clientWidth / autoScale - root.width()) >> 1) * this._scale;
167 this._paddingY = ((this._clientHeight / autoScale - root.height()) >> 1) * this._scale;
164 const screenLayerSpacing = 20; 168 const screenLayerSpacing = 20;
165 this._layerSpacing = Math.ceil(screenLayerSpacing / this._scale) + "px"; 169 this._layerSpacing = screenLayerSpacing + "px";
166 const screenLayerThickness = 4; 170 const screenLayerThickness = 4;
167 var layerThickness = Math.ceil(screenLayerThickness / this._scale) + "px "; 171 var layerThickness = screenLayerThickness + "px";
172
168 var stylesheetContent = ".layer-container .side-wall { height: " + layer Thickness + "; width: " + layerThickness + "; } " + 173 var stylesheetContent = ".layer-container .side-wall { height: " + layer Thickness + "; width: " + layerThickness + "; } " +
169 ".layer-container .back-wall { -webkit-transform: translateZ(-" + la yerThickness + "); } " + 174 ".layer-container .back-wall { -webkit-transform: translateZ(-" + la yerThickness + "); } " +
170 ".layer-container { -webkit-transform: translateZ(" + this._layerSpa cing + "); }"; 175 ".layer-container { -webkit-transform: translateZ(" + this._layerSpa cing + "); }";
171 // Workaround for double style recalculation upon assignment to style sh eet's text content. 176 // Workaround for double style recalculation upon assignment to style sh eet's text content.
172 var stylesheetTextNode = this._scaleAdjustmentStylesheet.firstChild; 177 var stylesheetTextNode = this._scaleAdjustmentStylesheet.firstChild;
173 if (!stylesheetTextNode || stylesheetTextNode.nodeType !== Node.TEXT_NOD E || stylesheetTextNode.nextSibling) 178 if (!stylesheetTextNode || stylesheetTextNode.nodeType !== Node.TEXT_NOD E || stylesheetTextNode.nextSibling)
174 this._scaleAdjustmentStylesheet.textContent = stylesheetContent; 179 this._scaleAdjustmentStylesheet.textContent = stylesheetContent;
175 else 180 else
176 stylesheetTextNode.nodeValue = stylesheetContent; 181 stylesheetTextNode.nodeValue = stylesheetContent;
177 var element = this._elementForLayer(root); 182
178 element.style.webkitTransform = "scale3d(" + this._scale + "," + this._s cale + "," + this._scale + ")"; 183 var style = this._elementForLayer(root).style;
179 element.style.webkitTransformOrigin = ""; 184 style.left = Math.round(this._paddingX) + "px";
180 element.style.left = ((this._clientWidth - root.width() * this._scale) > > 1) + "px"; 185 style.top = Math.round(this._paddingY) + "px";
181 element.style.top = ((this._clientHeight - root.height() * this._scale) >> 1) + "px"; 186 style.webkitTransformOrigin = "";
187 },
188
189 /**
190 * @param {!WebInspector.Event} event
191 */
192 _onTransformChanged: function(event)
193 {
194 var changedTransforms = /** @type {number} */ (event.data);
195 if (changedTransforms & WebInspector.TransformController.TransformType.S cale)
196 this._update();
197 else
198 this._updateTransform();
199 },
200
201 _updateTransform: function()
202 {
203 var root = this._model.contentRoot();
204 if (!root)
205 return;
206 var offsetX = this._transformController.offsetX();
207 var offsetY = this._transformController.offsetY();
208 var style = this._rotatingContainerElement.style;
209 // Translate well to front so that no matter how we turn the plane, no p arts of it goes below parent.
210 // This makes sure mouse events go to proper layers, not straight to the parent.
211 style.webkitTransform = "translateZ(10000px)" +
212 " rotateX(" + this._transformController.rotateX() + "deg) rotateY(" + this._transformController.rotateY() + "deg)" +
213 " translateX(" + offsetX + "px) translateY(" + offsetY + "px)";
214 // Compute where the center of shitfted and scaled root layer would be a nd use is as origin for rotation.
215 style.webkitTransformOrigin = Math.round(this._paddingX + offsetX + root .width() * this._scale / 2) + "px " + Math.round(this._paddingY + offsetY + root .height() * this._scale / 2) + "px";
182 }, 216 },
183 217
184 _update: function() 218 _update: function()
185 { 219 {
186 if (!this.isShowing()) { 220 if (!this.isShowing()) {
187 this._needsUpdate = true; 221 this._needsUpdate = true;
188 return; 222 return;
189 } 223 }
190 if (!this._model.contentRoot()) { 224 if (!this._model.contentRoot()) {
191 this._emptyView.show(this.element); 225 this._emptyView.show(this.element);
(...skipping 11 matching lines...) Expand all
203 } 237 }
204 this._clientWidth = this.element.clientWidth; 238 this._clientWidth = this.element.clientWidth;
205 this._clientHeight = this.element.clientHeight; 239 this._clientHeight = this.element.clientHeight;
206 for (var layerId in this._elementsByLayerId) { 240 for (var layerId in this._elementsByLayerId) {
207 if (this._model.layerById(layerId)) 241 if (this._model.layerById(layerId))
208 continue; 242 continue;
209 this._elementsByLayerId[layerId].remove(); 243 this._elementsByLayerId[layerId].remove();
210 delete this._elementsByLayerId[layerId]; 244 delete this._elementsByLayerId[layerId];
211 } 245 }
212 this._scaleToFit(); 246 this._scaleToFit();
247 this._updateTransform();
213 this._model.forEachLayer(updateLayer.bind(this), this._model.contentRoot ()); 248 this._model.forEachLayer(updateLayer.bind(this), this._model.contentRoot ());
214 this._needsUpdate = false; 249 this._needsUpdate = false;
215 }, 250 },
216 251
217 /** 252 /**
218 * @param {!WebInspector.Event} event 253 * @param {!WebInspector.Event} event
219 */ 254 */
220 _onLayerPainted: function(event) 255 _onLayerPainted: function(event)
221 { 256 {
222 var layer = /** @type {!WebInspector.Layer} */ (event.data); 257 var layer = /** @type {!WebInspector.Layer} */ (event.data);
(...skipping 29 matching lines...) Expand all
252 var layer = element.__layerDetails.layer; 287 var layer = element.__layerDetails.layer;
253 var style = element.style; 288 var style = element.style;
254 var isContentRoot = layer === this._model.contentRoot(); 289 var isContentRoot = layer === this._model.contentRoot();
255 var parentElement = isContentRoot ? this._rotatingContainerElement : thi s._elementForLayer(layer.parent()); 290 var parentElement = isContentRoot ? this._rotatingContainerElement : thi s._elementForLayer(layer.parent());
256 element.__layerDetails.depth = parentElement.__layerDetails ? parentElem ent.__layerDetails.depth + 1 : 0; 291 element.__layerDetails.depth = parentElement.__layerDetails ? parentElem ent.__layerDetails.depth + 1 : 0;
257 element.classList.toggle("invisible", layer.invisible()); 292 element.classList.toggle("invisible", layer.invisible());
258 this._updateElementColor(element); 293 this._updateElementColor(element);
259 if (parentElement !== element.parentElement) 294 if (parentElement !== element.parentElement)
260 parentElement.appendChild(element); 295 parentElement.appendChild(element);
261 296
262 style.width = layer.width() + "px"; 297 style.width = Math.round(layer.width() * this._scale) + "px";
263 style.height = layer.height() + "px"; 298 style.height = Math.round(layer.height() * this._scale) + "px";
264 this._updatePaintRect(element); 299 this._updatePaintRect(element);
265 if (isContentRoot) 300 if (isContentRoot)
266 return; 301 return;
267 302 style.left = Math.round(layer.offsetX() * this._scale) + "px";
268 style.left = layer.offsetX() + "px"; 303 style.top = Math.round(layer.offsetY() * this._scale) + "px";
269 style.top = layer.offsetY() + "px";
270 var transform = layer.transform(); 304 var transform = layer.transform();
271 if (transform) { 305 if (transform) {
306 transform = transform.slice();
307 // Adjust offset in the transform matrix according to scale.
308 for (var i = 12; i < 15; ++i)
309 transform[i] *= this._scale;
272 // Avoid exponential notation in CSS. 310 // Avoid exponential notation in CSS.
273 style.webkitTransform = "matrix3d(" + transform.map(toFixed5).join(" ,") + ") translateZ(" + this._layerSpacing + ")"; 311 style.webkitTransform = "matrix3d(" + transform.map(toFixed5).join(" ,") + ") translateZ(" + this._layerSpacing + ")";
274 var anchor = layer.anchorPoint(); 312 var anchor = layer.anchorPoint();
275 style.webkitTransformOrigin = Math.round(anchor[0] * 100) + "% " + M ath.round(anchor[1] * 100) + "% " + anchor[2]; 313 style.webkitTransformOrigin = Math.round(anchor[0] * 100) + "% " + M ath.round(anchor[1] * 100) + "% " + anchor[2];
276 } else { 314 } else {
277 style.webkitTransform = ""; 315 style.webkitTransform = "";
278 style.webkitTransformOrigin = ""; 316 style.webkitTransformOrigin = "";
279 } 317 }
280 318
281 function toFixed5(x) 319 function toFixed5(x)
282 { 320 {
283 return x.toFixed(5); 321 return x.toFixed(5);
284 } 322 }
285 }, 323 },
286 324
287 _updatePaintRect: function(element) 325 _updatePaintRect: function(element)
288 { 326 {
289 var details = element.__layerDetails; 327 var details = element.__layerDetails;
290 var paintRect = details.layer.lastPaintRect(); 328 var paintRect = details.layer.lastPaintRect();
291 var paintRectElement = details.paintRectElement; 329 var paintRectElement = details.paintRectElement;
292 if (!paintRect || !WebInspector.settings.showPaintRects.get()) { 330 if (!paintRect || !WebInspector.settings.showPaintRects.get()) {
293 paintRectElement.classList.add("hidden"); 331 paintRectElement.classList.add("hidden");
294 return; 332 return;
295 } 333 }
296 paintRectElement.classList.remove("hidden"); 334 paintRectElement.classList.remove("hidden");
297 if (details.paintCount === details.layer.paintCount()) 335 if (details.paintCount === details.layer.paintCount())
298 return; 336 return;
299 details.paintCount = details.layer.paintCount(); 337 details.paintCount = details.layer.paintCount();
300 var style = paintRectElement.style; 338 var style = paintRectElement.style;
301 style.left = paintRect.x + "px"; 339 style.left = Math.round(paintRect.x * this._scale) + "px";
302 style.top = paintRect.y + "px"; 340 style.top = Math.round(paintRect.y * this._scale) + "px";
303 style.width = paintRect.width + "px"; 341 style.width = Math.round(paintRect.width * this._scale) + "px";
304 style.height = paintRect.height + "px"; 342 style.height = Math.round(paintRect.height * this._scale) + "px";
305 var color = WebInspector.Layers3DView.PaintRectColors[details.paintCount % WebInspector.Layers3DView.PaintRectColors.length]; 343 var color = WebInspector.Layers3DView.PaintRectColors[details.paintCount % WebInspector.Layers3DView.PaintRectColors.length];
306 style.borderWidth = Math.ceil(1 / this._scale) + "px"; 344 style.borderWidth = Math.ceil(1 / this._scale) + "px";
307 style.borderColor = color.toString(WebInspector.Color.Format.RGBA); 345 style.borderColor = color.toString(WebInspector.Color.Format.RGBA);
308 }, 346 },
309 347
310 /** 348 /**
311 * @param {!Element} element 349 * @param {!Element} element
312 */ 350 */
313 _updateElementColor: function(element) 351 _updateElementColor: function(element)
314 { 352 {
315 var color; 353 var color;
316 if (element === this._lastOutlinedElement[WebInspector.Layers3DView.Outl ineType.Selected]) 354 if (element === this._lastOutlinedElement[WebInspector.Layers3DView.Outl ineType.Selected])
317 color = WebInspector.Color.PageHighlight.Content.toString(WebInspect or.Color.Format.RGBA) || ""; 355 color = WebInspector.Color.PageHighlight.Content.toString(WebInspect or.Color.Format.RGBA) || "";
318 else { 356 else {
319 const base = 144; 357 const base = 144;
320 var component = base + 20 * ((element.__layerDetails.depth - 1) % 5) ; 358 var component = base + 20 * ((element.__layerDetails.depth - 1) % 5) ;
321 color = "rgba(" + component + "," + component + "," + component + ", 0.8)"; 359 color = "rgba(" + component + "," + component + "," + component + ", 0.8)";
322 } 360 }
323 element.style.backgroundColor = color; 361 element.style.backgroundColor = color;
324 }, 362 },
325 363
326 /** 364 /**
327 * @param {?Event} event 365 * @param {?Event} event
328 */
329 _onMouseDown: function(event)
330 {
331 if (event.which !== 1)
332 return;
333 this._setReferencePoint(event);
334 },
335
336 /**
337 * @param {?Event} event
338 */
339 _setReferencePoint: function(event)
340 {
341 this._originX = event.clientX;
342 this._originY = event.clientY;
343 this._oldRotateX = this._rotateX;
344 this._oldRotateY = this._rotateY;
345 },
346
347 _resetReferencePoint: function()
348 {
349 delete this._originX;
350 delete this._originY;
351 delete this._oldRotateX;
352 delete this._oldRotateY;
353 },
354
355 /**
356 * @param {?Event} event
357 */
358 _onMouseUp: function(event)
359 {
360 if (event.which !== 1)
361 return;
362 this._resetReferencePoint();
363 },
364
365 /**
366 * @param {?Event} event
367 * @return {?WebInspector.Layer} 366 * @return {?WebInspector.Layer}
368 */ 367 */
369 _layerFromEventPoint: function(event) 368 _layerFromEventPoint: function(event)
370 { 369 {
371 var element = this.element.ownerDocument.elementFromPoint(event.pageX, e vent.pageY); 370 var element = this.element.ownerDocument.elementFromPoint(event.pageX, e vent.pageY);
372 if (!element) 371 if (!element)
373 return null; 372 return null;
374 element = element.enclosingNodeOrSelfWithClass("layer-container"); 373 element = element.enclosingNodeOrSelfWithClass("layer-container");
375 return element && element.__layerDetails && element.__layerDetails.layer ; 374 return element && element.__layerDetails && element.__layerDetails.layer ;
376 }, 375 },
377 376
378 /** 377 /**
379 * @param {?Event} event 378 * @param {?Event} event
380 */ 379 */
381 _onMouseMove: function(event)
382 {
383 if (!event.which) {
384 this.dispatchEventToListeners(WebInspector.Layers3DView.Events.Layer Hovered, this._layerFromEventPoint(event));
385 return;
386 }
387 if (event.which === 1) {
388 // Set reference point if we missed mousedown.
389 if (typeof this._originX !== "number")
390 this._setReferencePoint(event);
391 this._rotateX = this._oldRotateX + (this._originY - event.clientY) / 2;
392 this._rotateY = this._oldRotateY - (this._originX - event.clientX) / 4;
393 // Translate well to front so that no matter how we turn the plane, no parts of it goes below parent.
394 // This makes sure mouse events go to proper layers, not straight to the parent.
395 this._rotatingContainerElement.style.webkitTransform = "translateZ(1 0000px) rotateX(" + this._rotateX + "deg) rotateY(" + this._rotateY + "deg)";
396 }
397 },
398
399 /**
400 * @param {?Event} event
401 */
402 _onContextMenu: function(event) 380 _onContextMenu: function(event)
403 { 381 {
404 var layer = this._layerFromEventPoint(event); 382 var layer = this._layerFromEventPoint(event);
405 var nodeId = layer && layer.nodeId(); 383 var nodeId = layer && layer.nodeId();
406 if (!nodeId) 384 if (!nodeId)
407 return; 385 return;
408 var domNode = WebInspector.domAgent.nodeForId(nodeId); 386 var domNode = WebInspector.domAgent.nodeForId(nodeId);
409 if (!domNode) 387 if (!domNode)
410 return; 388 return;
411 var contextMenu = new WebInspector.ContextMenu(event); 389 var contextMenu = new WebInspector.ContextMenu(event);
412 contextMenu.appendApplicableItems(domNode); 390 contextMenu.appendApplicableItems(domNode);
413 contextMenu.show(); 391 contextMenu.show();
414 }, 392 },
415 393
416 /** 394 /**
417 * @param {?Event} event 395 * @param {?Event} event
418 */ 396 */
397 _onMouseMove: function(event)
398 {
399 if (event.which)
400 return;
401 this.dispatchEventToListeners(WebInspector.Layers3DView.Events.LayerHove red, this._layerFromEventPoint(event));
402 },
403
404 /**
405 * @param {?Event} event
406 */
419 _onClick: function(event) 407 _onClick: function(event)
420 { 408 {
421 this.dispatchEventToListeners(WebInspector.Layers3DView.Events.LayerSele cted, this._layerFromEventPoint(event)); 409 this.dispatchEventToListeners(WebInspector.Layers3DView.Events.LayerSele cted, this._layerFromEventPoint(event));
422 }, 410 },
423 411
424 /** 412 /**
425 * @param {?Event} event 413 * @param {?Event} event
426 */ 414 */
427 _onDoubleClick: function(event) 415 _onDoubleClick: function(event)
428 { 416 {
(...skipping 11 matching lines...) Expand all
440 * @param {!WebInspector.Layer} layer 428 * @param {!WebInspector.Layer} layer
441 * @param {!Element} paintRectElement 429 * @param {!Element} paintRectElement
442 */ 430 */
443 WebInspector.LayerDetails = function(layer, paintRectElement) 431 WebInspector.LayerDetails = function(layer, paintRectElement)
444 { 432 {
445 this.layer = layer; 433 this.layer = layer;
446 this.depth = 0; 434 this.depth = 0;
447 this.paintRectElement = paintRectElement; 435 this.paintRectElement = paintRectElement;
448 this.paintCount = 0; 436 this.paintCount = 0;
449 } 437 }
OLDNEW
« no previous file with comments | « Source/devtools/devtools.gypi ('k') | Source/devtools/front_end/LayersPanel.js » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698