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

Side by Side Diff: third_party/WebKit/Source/devtools/front_end/emulation/DeviceModeView.js

Issue 1969913002: [DevTools] Polish device mode UI, include outline into screenshot. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: cleanup Created 4 years, 7 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
OLDNEW
1 // Copyright 2015 The Chromium Authors. All rights reserved. 1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 /** 5 /**
6 * @constructor 6 * @constructor
7 * @extends {WebInspector.VBox} 7 * @extends {WebInspector.VBox}
8 */ 8 */
9 WebInspector.DeviceModeView = function() 9 WebInspector.DeviceModeView = function()
10 { 10 {
(...skipping 26 matching lines...) Expand all
37 37
38 this._contentClip = this.contentElement.createChild("div", "device-mode- content-clip vbox"); 38 this._contentClip = this.contentElement.createChild("div", "device-mode- content-clip vbox");
39 this._responsivePresetsContainer = this._contentClip.createChild("div", "device-mode-presets-container"); 39 this._responsivePresetsContainer = this._contentClip.createChild("div", "device-mode-presets-container");
40 this._populatePresetsContainer(); 40 this._populatePresetsContainer();
41 this._mediaInspectorContainer = this._contentClip.createChild("div", "de vice-mode-media-container"); 41 this._mediaInspectorContainer = this._contentClip.createChild("div", "de vice-mode-media-container");
42 this._contentArea = this._contentClip.createChild("div", "device-mode-co ntent-area"); 42 this._contentArea = this._contentClip.createChild("div", "device-mode-co ntent-area");
43 43
44 this._outlineImage = this._contentArea.createChild("img", "device-mode-o utline-image hidden fill"); 44 this._outlineImage = this._contentArea.createChild("img", "device-mode-o utline-image hidden fill");
45 this._outlineImage.addEventListener("load", this._onImageLoaded.bind(thi s, this._outlineImage, true), false); 45 this._outlineImage.addEventListener("load", this._onImageLoaded.bind(thi s, this._outlineImage, true), false);
46 this._outlineImage.addEventListener("error", this._onImageLoaded.bind(th is, this._outlineImage, false), false); 46 this._outlineImage.addEventListener("error", this._onImageLoaded.bind(th is, this._outlineImage, false), false);
47 this._outlineImage.classList.toggle("device-frame-visible", this._model. deviceOutlineSetting().get());
48 47
49 this._screenArea = this._contentArea.createChild("div", "device-mode-scr een-area"); 48 this._screenArea = this._contentArea.createChild("div", "device-mode-scr een-area");
50 this._screenImage = this._screenArea.createChild("img", "device-mode-scr een-image hidden"); 49 this._screenImage = this._screenArea.createChild("img", "device-mode-scr een-image hidden");
51 this._screenImage.addEventListener("load", this._onImageLoaded.bind(this , this._screenImage, true), false); 50 this._screenImage.addEventListener("load", this._onImageLoaded.bind(this , this._screenImage, true), false);
52 this._screenImage.addEventListener("error", this._onImageLoaded.bind(thi s, this._screenImage, false), false); 51 this._screenImage.addEventListener("error", this._onImageLoaded.bind(thi s, this._screenImage, false), false);
53 52
54 this._bottomRightResizerElement = this._screenArea.createChild("div", "d evice-mode-resizer device-mode-bottom-right-resizer"); 53 this._bottomRightResizerElement = this._screenArea.createChild("div", "d evice-mode-resizer device-mode-bottom-right-resizer");
55 this._bottomRightResizerElement.createChild("div", ""); 54 this._bottomRightResizerElement.createChild("div", "");
56 this._createResizer(this._bottomRightResizerElement, 2, 1); 55 this._createResizer(this._bottomRightResizerElement, 2, 1);
57 56
(...skipping 21 matching lines...) Expand all
79 78
80 _populatePresetsContainer: function() 79 _populatePresetsContainer: function()
81 { 80 {
82 var sizes = [320, 375, 425, 768, 1024, 1440, 2560]; 81 var sizes = [320, 375, 425, 768, 1024, 1440, 2560];
83 var titles = [WebInspector.UIString("Mobile S"), 82 var titles = [WebInspector.UIString("Mobile S"),
84 WebInspector.UIString("Mobile M"), 83 WebInspector.UIString("Mobile M"),
85 WebInspector.UIString("Mobile L"), 84 WebInspector.UIString("Mobile L"),
86 WebInspector.UIString("Tablet"), 85 WebInspector.UIString("Tablet"),
87 WebInspector.UIString("Laptop"), 86 WebInspector.UIString("Laptop"),
88 WebInspector.UIString("Laptop L"), 87 WebInspector.UIString("Laptop L"),
89 WebInspector.UIString("4K")] 88 WebInspector.UIString("4K")];
90 this._presetBlocks = []; 89 this._presetBlocks = [];
91 var inner = this._responsivePresetsContainer.createChild("div", "device- mode-presets-container-inner") 90 var inner = this._responsivePresetsContainer.createChild("div", "device- mode-presets-container-inner");
92 for (var i = sizes.length - 1; i >= 0; --i) { 91 for (var i = sizes.length - 1; i >= 0; --i) {
93 var outer = inner.createChild("div", "fill device-mode-preset-bar-ou ter"); 92 var outer = inner.createChild("div", "fill device-mode-preset-bar-ou ter");
94 var block = outer.createChild("div", "device-mode-preset-bar"); 93 var block = outer.createChild("div", "device-mode-preset-bar");
95 block.createChild("span").textContent = titles[i] + " \u2013 " + siz es[i] + "px"; 94 block.createChild("span").textContent = titles[i] + " \u2013 " + siz es[i] + "px";
96 block.addEventListener("click", applySize.bind(this, sizes[i]), fals e); 95 block.addEventListener("click", applySize.bind(this, sizes[i]), fals e);
97 block.__width = sizes[i]; 96 block.__width = sizes[i];
98 this._presetBlocks.push(block); 97 this._presetBlocks.push(block);
99 } 98 }
100 99
101 /** 100 /**
102 * @param {number} width 101 * @param {number} width
103 * @param {!Event} e 102 * @param {!Event} e
104 * @this {WebInspector.DeviceModeView} 103 * @this {WebInspector.DeviceModeView}
105 */ 104 */
106 function applySize(width, e) 105 function applySize(width, e)
107 { 106 {
108 this._model.emulate(WebInspector.DeviceModeModel.Type.Responsive, nu ll, null); 107 this._model.emulate(WebInspector.DeviceModeModel.Type.Responsive, nu ll, null);
109 this._model.setSizeAndScaleToFit(width, 0); 108 this._model.setSizeAndScaleToFit(width, 0);
110 e.consume(); 109 e.consume();
111 } 110 }
112 }, 111 },
113 112
114 toggleDeviceMode: function()
115 {
116 this._toolbar.toggleDeviceMode();
117 },
118
119 /** 113 /**
120 * @param {!Element} element 114 * @param {!Element} element
121 * @param {number} widthFactor 115 * @param {number} widthFactor
122 * @param {number} heightFactor 116 * @param {number} heightFactor
123 * @return {!WebInspector.ResizerWidget} 117 * @return {!WebInspector.ResizerWidget}
124 */ 118 */
125 _createResizer: function(element, widthFactor, heightFactor) 119 _createResizer: function(element, widthFactor, heightFactor)
126 { 120 {
127 var resizer = new WebInspector.ResizerWidget(); 121 var resizer = new WebInspector.ResizerWidget();
128 resizer.addElement(element); 122 resizer.addElement(element);
(...skipping 74 matching lines...) Expand 10 before | Expand all | Expand 10 after
203 element.style.top = rect.top + "px"; 197 element.style.top = rect.top + "px";
204 element.style.width = rect.width + "px"; 198 element.style.width = rect.width + "px";
205 element.style.height = rect.height + "px"; 199 element.style.height = rect.height + "px";
206 } 200 }
207 201
208 if (!this.isShowing()) 202 if (!this.isShowing())
209 return; 203 return;
210 204
211 var zoomFactor = WebInspector.zoomManager.zoomFactor(); 205 var zoomFactor = WebInspector.zoomManager.zoomFactor();
212 var callDoResize = false; 206 var callDoResize = false;
213 var showRulers = this._showRulersSetting.get() && !this._model.deviceOut lineSetting().get() && this._model.type() !== WebInspector.DeviceModeModel.Type. None; 207 var showRulers = this._showRulersSetting.get() && this._model.type() !== WebInspector.DeviceModeModel.Type.None;
214 var contentAreaResized = false; 208 var contentAreaResized = false;
215 var updateRulers = false; 209 var updateRulers = false;
216 210
217 var cssScreenRect = this._model.screenRect().scale(1 / zoomFactor); 211 var cssScreenRect = this._model.screenRect().scale(1 / zoomFactor);
218 if (!cssScreenRect.isEqual(this._cachedCssScreenRect)) { 212 if (!cssScreenRect.isEqual(this._cachedCssScreenRect)) {
219 applyRect(this._screenArea, cssScreenRect); 213 applyRect(this._screenArea, cssScreenRect);
220 this._leftRuler.element.style.left = cssScreenRect.left + "px";
221 updateRulers = true; 214 updateRulers = true;
222 callDoResize = true; 215 callDoResize = true;
223 this._cachedCssScreenRect = cssScreenRect; 216 this._cachedCssScreenRect = cssScreenRect;
224 } 217 }
225 218
226 var cssVisiblePageRect = this._model.visiblePageRect().scale(1 / zoomFac tor); 219 var cssVisiblePageRect = this._model.visiblePageRect().scale(1 / zoomFac tor);
227 if (!cssVisiblePageRect.isEqual(this._cachedCssVisiblePageRect)) { 220 if (!cssVisiblePageRect.isEqual(this._cachedCssVisiblePageRect)) {
228 applyRect(this._pageArea, cssVisiblePageRect); 221 applyRect(this._pageArea, cssVisiblePageRect);
229 callDoResize = true; 222 callDoResize = true;
230 this._cachedCssVisiblePageRect = cssVisiblePageRect; 223 this._cachedCssVisiblePageRect = cssVisiblePageRect;
231 } 224 }
232 225
233 var outlineRect = this._model.outlineRect().scale(1 / zoomFactor); 226 var outlineRect = this._model.outlineRect().scale(1 / zoomFactor);
234 if (!outlineRect.isEqual(this._cachedOutlineRect)) { 227 if (!outlineRect.isEqual(this._cachedOutlineRect)) {
235 applyRect(this._outlineImage, outlineRect); 228 applyRect(this._outlineImage, outlineRect);
236 callDoResize = true; 229 callDoResize = true;
237 this._cachedOutlineRect = outlineRect; 230 this._cachedOutlineRect = outlineRect;
238 } 231 }
239 this._outlineImage.classList.toggle("device-frame-visible", (this._model .deviceOutlineSetting().get() && this._model.outlineImage())); 232 this._contentClip.classList.toggle("device-mode-outline-visible", !!this ._model.outlineImage());
240 233
241 var resizable = this._model.type() === WebInspector.DeviceModeModel.Type .Responsive; 234 var resizable = this._model.type() === WebInspector.DeviceModeModel.Type .Responsive;
242 if (resizable !== this._cachedResizable) { 235 if (resizable !== this._cachedResizable) {
243 this._rightResizerElement.classList.toggle("hidden", !resizable); 236 this._rightResizerElement.classList.toggle("hidden", !resizable);
244 this._leftResizerElement.classList.toggle("hidden", !resizable); 237 this._leftResizerElement.classList.toggle("hidden", !resizable);
245 this._bottomResizerElement.classList.toggle("hidden", !resizable); 238 this._bottomResizerElement.classList.toggle("hidden", !resizable);
246 this._bottomRightResizerElement.classList.toggle("hidden", !resizabl e); 239 this._bottomRightResizerElement.classList.toggle("hidden", !resizabl e);
247 this._bottomLeftResizerElement.classList.toggle("hidden", !resizable ); 240 this._bottomLeftResizerElement.classList.toggle("hidden", !resizable );
248 this._cachedResizable = resizable; 241 this._cachedResizable = resizable;
249 } 242 }
(...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after
281 this._cachedScale = this._model.scale(); 274 this._cachedScale = this._model.scale();
282 } 275 }
283 276
284 this._toolbar.update(); 277 this._toolbar.update();
285 this._loadImage(this._screenImage, this._model.screenImage()); 278 this._loadImage(this._screenImage, this._model.screenImage());
286 this._loadImage(this._outlineImage, this._model.outlineImage()); 279 this._loadImage(this._outlineImage, this._model.outlineImage());
287 this._mediaInspector.setAxisTransform(this._model.scale()); 280 this._mediaInspector.setAxisTransform(this._model.scale());
288 if (callDoResize) 281 if (callDoResize)
289 this.doResize(); 282 this.doResize();
290 if (updateRulers) { 283 if (updateRulers) {
291 this._topRuler.render(this._cachedCssScreenRect ? this._cachedCssScr eenRect.left : 0, this._model.scale()); 284 this._topRuler.render(this._model.scale());
292 this._leftRuler.render(0, this._model.scale()); 285 this._leftRuler.render(this._model.scale());
293 this._topRuler.element.style.top = this._cachedCssScreenRect ? this. _cachedCssScreenRect.top + "px" : "0"; 286 this._topRuler.element.positionAt(this._cachedCssScreenRect ? this._ cachedCssScreenRect.left : 0, this._cachedCssScreenRect ? this._cachedCssScreenR ect.top : 0);
294 this._leftRuler.element.style.top = this._cachedCssScreenRect ? this ._cachedCssScreenRect.top + "px" : "0"; 287 this._leftRuler.element.positionAt(this._cachedCssScreenRect ? this. _cachedCssScreenRect.left : 0, this._cachedCssScreenRect ? this._cachedCssScreen Rect.top : 0);
295 } 288 }
296 if (contentAreaResized) 289 if (contentAreaResized)
297 this._contentAreaResized(); 290 this._contentAreaResized();
298 }, 291 },
299 292
300 /** 293 /**
301 * @param {!Element} element 294 * @param {!Element} element
302 * @param {string} srcset 295 * @param {string} srcset
303 */ 296 */
304 _loadImage: function(element, srcset) 297 _loadImage: function(element, srcset)
(...skipping 73 matching lines...) Expand 10 before | Expand all | Expand 10 after
378 captureScreenshot: function() 371 captureScreenshot: function()
379 { 372 {
380 var mainTarget = WebInspector.targetManager.mainTarget(); 373 var mainTarget = WebInspector.targetManager.mainTarget();
381 if (!mainTarget) 374 if (!mainTarget)
382 return; 375 return;
383 WebInspector.DOMModel.muteHighlight(); 376 WebInspector.DOMModel.muteHighlight();
384 377
385 var zoomFactor = WebInspector.zoomManager.zoomFactor(); 378 var zoomFactor = WebInspector.zoomManager.zoomFactor();
386 var rect = this._contentArea.getBoundingClientRect(); 379 var rect = this._contentArea.getBoundingClientRect();
387 var availableSize = new Size(Math.max(rect.width * zoomFactor, 1), Math. max(rect.height * zoomFactor, 1)); 380 var availableSize = new Size(Math.max(rect.width * zoomFactor, 1), Math. max(rect.height * zoomFactor, 1));
381 var outlineVisible = this._model.deviceOutlineSetting().get();
388 382
389 if (availableSize.width < this._model.screenRect().width || 383 if (availableSize.width < this._model.screenRect().width ||
390 availableSize.height < this._model.screenRect().height) { 384 availableSize.height < this._model.screenRect().height) {
391 WebInspector.inspectorView.minimize(); 385 WebInspector.inspectorView.minimize();
386 this._model.deviceOutlineSetting().set(false);
392 } 387 }
393 388
394 mainTarget.pageAgent().captureScreenshot(screenshotCaptured.bind(this)); 389 mainTarget.pageAgent().captureScreenshot(screenshotCaptured.bind(this));
395 390
396 /** 391 /**
397 * @param {?Protocol.Error} error 392 * @param {?Protocol.Error} error
398 * @param {string} content 393 * @param {string} content
399 * @this {WebInspector.DeviceModeView} 394 * @this {WebInspector.DeviceModeView}
400 */ 395 */
401 function screenshotCaptured(error, content) 396 function screenshotCaptured(error, content)
402 { 397 {
403 if (error) { 398 this._model.deviceOutlineSetting().set(outlineVisible);
404 WebInspector.DOMModel.unmuteHighlight(); 399 var dpr = window.devicePixelRatio;
405 WebInspector.inspectorView.restore(); 400 var outlineRect = this._model.outlineRect().scale(dpr);
401 var screenRect = this._model.screenRect().scale(dpr);
402 screenRect.left -= outlineRect.left;
403 screenRect.top -= outlineRect.top;
404 var visiblePageRect = this._model.visiblePageRect().scale(dpr);
405 visiblePageRect.left += screenRect.left;
406 visiblePageRect.top += screenRect.top;
407 outlineRect.left = 0;
408 outlineRect.top = 0;
409
410 WebInspector.DOMModel.unmuteHighlight();
411 WebInspector.inspectorView.restore();
412
413 if (error)
406 return; 414 return;
407 }
408 415
409 // Create a canvas to splice the images together. 416 // Create a canvas to splice the images together.
410 var canvas = createElement("canvas"); 417 var canvas = createElement("canvas");
411 var ctx = canvas.getContext("2d"); 418 var ctx = canvas.getContext("2d");
412 var screenRect = this._model.screenRect(); 419 canvas.width = outlineRect.width;
413 var dpr = window.devicePixelRatio; 420 canvas.height = outlineRect.height;
414 canvas.width = screenRect.width * dpr; 421
415 canvas.height = screenRect.height * dpr; 422 var promise = Promise.resolve();
416 // Add any available screen images. 423 if (this._model.outlineImage())
417 if (this._model.screenImage()) { 424 promise = promise.then(paintImage.bind(null, this._model.outline Image(), outlineRect));
418 var screenImage = new Image(); 425 if (this._model.screenImage())
419 screenImage.crossOrigin = "Anonymous"; 426 promise = promise.then(paintImage.bind(null, this._model.screenI mage(), screenRect));
420 screenImage.srcset = this._model.screenImage(); 427 promise.then(paintScreenshot.bind(this));
421 screenImage.onload = onImageLoad.bind(this); 428
422 screenImage.onerror = paintScreenshot.bind(this); 429 /**
423 } else { 430 * @param {string} src
424 paintScreenshot.call(this); 431 * @param {!WebInspector.Rect} rect
432 * @return {!Promise<undefined>}
433 */
434 function paintImage(src, rect)
435 {
436 var callback;
437 var promise = new Promise(fulfill => callback = fulfill);
438 var image = new Image();
439 image.crossOrigin = "Anonymous";
440 image.srcset = src;
441 image.onload = onImageLoad;
442 image.onerror = callback;
443 return promise;
444
445 function onImageLoad()
446 {
447 ctx.drawImage(image, rect.left, rect.top, rect.width, rect.h eight);
448 callback();
449 }
425 } 450 }
426 451
427 /** 452 /**
428 * @this {WebInspector.DeviceModeView}
429 */
430 function onImageLoad()
431 {
432 ctx.drawImage(screenImage, 0, 0, screenRect.width * dpr, screenR ect.height * dpr);
433 paintScreenshot.call(this);
434 }
435
436 /**
437 * @this {WebInspector.DeviceModeView} 453 * @this {WebInspector.DeviceModeView}
438 */ 454 */
439 function paintScreenshot() 455 function paintScreenshot()
440 { 456 {
441 var pageImage = new Image(); 457 var pageImage = new Image();
442 pageImage.src = "data:image/png;base64," + content; 458 pageImage.src = "data:image/png;base64," + content;
443 var visiblePageRect = this._model.visiblePageRect().scale(dpr);
444 ctx.drawImage(pageImage, 459 ctx.drawImage(pageImage,
445 visiblePageRect.left, 460 visiblePageRect.left,
446 visiblePageRect.top, 461 visiblePageRect.top,
447 visiblePageRect.width, 462 visiblePageRect.width,
448 visiblePageRect.height); 463 visiblePageRect.height);
449 var mainFrame = mainTarget.resourceTreeModel.mainFrame; 464 var mainFrame = mainTarget.resourceTreeModel.mainFrame;
450 var fileName = mainFrame ? mainFrame.url.trimURL().removeURLFrag ment() : ""; 465 var fileName = mainFrame ? mainFrame.url.trimURL().removeURLFrag ment() : "";
451 if (this._model.type() === WebInspector.DeviceModeModel.Type.Dev ice) 466 if (this._model.type() === WebInspector.DeviceModeModel.Type.Dev ice)
452 fileName += WebInspector.UIString("(%s)", this._model.device ().title); 467 fileName += WebInspector.UIString("(%s)", this._model.device ().title);
453 // Trigger download. 468 // Trigger download.
454 var link = createElement("a"); 469 var link = createElement("a");
455 link.download = fileName + ".png"; 470 link.download = fileName + ".png";
456 link.href = canvas.toDataURL("image/png"); 471 link.href = canvas.toDataURL("image/png");
457 link.click(); 472 link.click();
458 WebInspector.DOMModel.unmuteHighlight();
459 WebInspector.inspectorView.restore();
460 } 473 }
461 } 474 }
462 }, 475 },
463 476
464 __proto__: WebInspector.VBox.prototype 477 __proto__: WebInspector.VBox.prototype
465 } 478 }
466 479
467 /** 480 /**
468 * @constructor 481 * @constructor
469 * @extends {WebInspector.VBox} 482 * @extends {WebInspector.VBox}
470 * @param {boolean} horizontal 483 * @param {boolean} horizontal
471 * @param {function(number)} applyCallback 484 * @param {function(number)} applyCallback
472 */ 485 */
473 WebInspector.DeviceModeView.Ruler = function(horizontal, applyCallback) 486 WebInspector.DeviceModeView.Ruler = function(horizontal, applyCallback)
474 { 487 {
475 WebInspector.VBox.call(this); 488 WebInspector.VBox.call(this);
476 this._contentElement = this.element.createChild("div", "device-mode-ruler fl ex-auto"); 489 this.element.classList.add("device-mode-ruler");
490 this._contentElement = this.element.createChild("div", "device-mode-ruler-co ntent").createChild("div", "device-mode-ruler-inner");
477 this._horizontal = horizontal; 491 this._horizontal = horizontal;
478 this._scale = 1; 492 this._scale = 1;
479 this._offset = 0;
480 this._count = 0; 493 this._count = 0;
481 this._throttler = new WebInspector.Throttler(0); 494 this._throttler = new WebInspector.Throttler(0);
482 this._applyCallback = applyCallback; 495 this._applyCallback = applyCallback;
483 } 496 }
484 497
485 WebInspector.DeviceModeView.Ruler.prototype = { 498 WebInspector.DeviceModeView.Ruler.prototype = {
486 /** 499 /**
487 * @param {number} offset
488 * @param {number} scale 500 * @param {number} scale
489 */ 501 */
490 render: function(offset, scale) 502 render: function(scale)
491 { 503 {
492 this._scale = scale; 504 this._scale = scale;
493 this._offset = offset;
494 if (this._horizontal)
495 this.element.style.paddingLeft = this._offset + "px";
496 else
497 this.element.style.paddingTop = this._offset + "px";
498 this._throttler.schedule(this._update.bind(this)); 505 this._throttler.schedule(this._update.bind(this));
499 }, 506 },
500 507
501 /** 508 /**
502 * @override 509 * @override
503 */ 510 */
504 onResize: function() 511 onResize: function()
505 { 512 {
506 this._throttler.schedule(this._update.bind(this)); 513 this._throttler.schedule(this._update.bind(this));
507 }, 514 },
(...skipping 56 matching lines...) Expand 10 before | Expand all | Expand 10 after
564 /** 571 /**
565 * @param {number} size 572 * @param {number} size
566 */ 573 */
567 _onMarkerClick: function(size) 574 _onMarkerClick: function(size)
568 { 575 {
569 this._applyCallback.call(null, size); 576 this._applyCallback.call(null, size);
570 }, 577 },
571 578
572 __proto__: WebInspector.VBox.prototype 579 __proto__: WebInspector.VBox.prototype
573 } 580 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698