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

Side by Side Diff: ui/message_center/cocoa/notification_controller.mm

Issue 23462005: Adds the contextMessage field to notifications. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Adjust unit test due to bugfix in toast layout. Created 7 years, 3 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
OLDNEW
1 // Copyright (c) 2013 The Chromium Authors. All rights reserved. 1 // Copyright (c) 2013 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 #import "ui/message_center/cocoa/notification_controller.h" 5 #import "ui/message_center/cocoa/notification_controller.h"
6 6
7 #include "base/mac/foundation_util.h" 7 #include "base/mac/foundation_util.h"
8 #include "base/strings/string_util.h" 8 #include "base/strings/string_util.h"
9 #include "base/strings/sys_string_conversions.h" 9 #include "base/strings/sys_string_conversions.h"
10 #include "base/strings/utf_string_conversions.h" 10 #include "base/strings/utf_string_conversions.h"
(...skipping 185 matching lines...) Expand 10 before | Expand all | Expand 10 after
196 196
197 // Initializes the closeButton_ ivar with the configured button. 197 // Initializes the closeButton_ ivar with the configured button.
198 - (void)configureCloseButtonInFrame:(NSRect)rootFrame; 198 - (void)configureCloseButtonInFrame:(NSRect)rootFrame;
199 199
200 // Initializes title_ in the given frame. 200 // Initializes title_ in the given frame.
201 - (void)configureTitleInFrame:(NSRect)rootFrame; 201 - (void)configureTitleInFrame:(NSRect)rootFrame;
202 202
203 // Initializes message_ in the given frame. 203 // Initializes message_ in the given frame.
204 - (void)configureBodyInFrame:(NSRect)rootFrame; 204 - (void)configureBodyInFrame:(NSRect)rootFrame;
205 205
206 // Initializes contextMessage_ in the given frame.
207 - (void)configureContextMessageInFrame:(NSRect)rootFrame;
208
206 // Creates a NSTextField that the caller owns configured as a label in a 209 // Creates a NSTextField that the caller owns configured as a label in a
207 // notification. 210 // notification.
208 - (NSTextField*)newLabelWithFrame:(NSRect)frame; 211 - (NSTextField*)newLabelWithFrame:(NSRect)frame;
209 212
210 // Gets the rectangle in which notification content should be placed. This 213 // Gets the rectangle in which notification content should be placed. This
211 // rectangle is to the right of the icon and left of the control buttons. 214 // rectangle is to the right of the icon and left of the control buttons.
212 // This depends on the icon_ and closeButton_ being initialized. 215 // This depends on the icon_ and closeButton_ being initialized.
213 - (NSRect)currentContentRect; 216 - (NSRect)currentContentRect;
214 217
215 // Returns the wrapped text that could fit within the given text field with not 218 // Returns the wrapped text that could fit within the given text field with not
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after
253 [rootView addSubview:closeButton_]; 256 [rootView addSubview:closeButton_];
254 257
255 // Create the title. 258 // Create the title.
256 [self configureTitleInFrame:rootFrame]; 259 [self configureTitleInFrame:rootFrame];
257 [rootView addSubview:title_]; 260 [rootView addSubview:title_];
258 261
259 // Create the message body. 262 // Create the message body.
260 [self configureBodyInFrame:rootFrame]; 263 [self configureBodyInFrame:rootFrame];
261 [rootView addSubview:message_]; 264 [rootView addSubview:message_];
262 265
266 // Create the context message body.
267 [self configureContextMessageInFrame:rootFrame];
268 [rootView addSubview:contextMessage_];
269
263 // Populate the data. 270 // Populate the data.
264 [self updateNotification:notification_]; 271 [self updateNotification:notification_];
265 } 272 }
266 273
267 - (NSRect)updateNotification:(const message_center::Notification*)notification { 274 - (NSRect)updateNotification:(const message_center::Notification*)notification {
268 DCHECK_EQ(notification->id(), notificationID_); 275 DCHECK_EQ(notification->id(), notificationID_);
269 notification_ = notification; 276 notification_ = notification;
270 277
271 NSRect rootFrame = NSMakeRect(0, 0, 278 NSRect rootFrame = NSMakeRect(0, 0,
272 message_center::kNotificationPreferredImageSize, 279 message_center::kNotificationPreferredImageSize,
273 message_center::kNotificationIconSize); 280 message_center::kNotificationIconSize);
274 281
275 // Update the icon. 282 // Update the icon.
276 [icon_ setImage:notification_->icon().AsNSImage()]; 283 [icon_ setImage:notification_->icon().AsNSImage()];
277 284
278 // The message_center:: constants are relative to capHeight at the top and 285 // The message_center:: constants are relative to capHeight at the top and
279 // relative to the baseline at the bottom, but NSTextField uses the full line 286 // relative to the baseline at the bottom, but NSTextField uses the full line
280 // height for its height. 287 // height for its height.
281 CGFloat titleTopGap = 288 CGFloat titleTopGap =
282 roundf([[title_ font] ascender] - [[title_ font] capHeight]); 289 roundf([[title_ font] ascender] - [[title_ font] capHeight]);
283 CGFloat titleBottomGap = roundf(fabs([[title_ font] descender])); 290 CGFloat titleBottomGap = roundf(fabs([[title_ font] descender]));
284 CGFloat titlePadding = message_center::kTextTopPadding - titleTopGap; 291 CGFloat titlePadding = message_center::kTextTopPadding - titleTopGap;
285 292
286 CGFloat messageTopGap = 293 CGFloat messageTopGap =
287 roundf([[message_ font] ascender] - [[message_ font] capHeight]); 294 roundf([[message_ font] ascender] - [[message_ font] capHeight]);
295 CGFloat messageBottomGap = roundf(fabs([[message_ font] descender]));
288 CGFloat messagePadding = 296 CGFloat messagePadding =
289 message_center::kTextTopPadding - titleBottomGap - messageTopGap; 297 message_center::kTextTopPadding - titleBottomGap - messageTopGap;
290 298
299 CGFloat contextMessageTopGap = roundf(
300 [[contextMessage_ font] ascender] - [[contextMessage_ font] capHeight]);
301 CGFloat contextMessagePadding =
302 message_center::kTextTopPadding - messageBottomGap - contextMessageTopGap;
303
291 // Set the title and recalculate the frame. 304 // Set the title and recalculate the frame.
292 [title_ setStringValue:base::SysUTF16ToNSString( 305 [title_ setStringValue:base::SysUTF16ToNSString(
293 [self wrapText:notification_->title() 306 [self wrapText:notification_->title()
294 forField:title_ 307 forField:title_
295 maxNumberOfLines:message_center::kTitleLineLimit])]; 308 maxNumberOfLines:message_center::kTitleLineLimit])];
296 [title_ sizeToFit]; 309 [title_ sizeToFit];
297 NSRect titleFrame = [title_ frame]; 310 NSRect titleFrame = [title_ frame];
298 titleFrame.origin.y = NSMaxY(rootFrame) - titlePadding - NSHeight(titleFrame); 311 titleFrame.origin.y = NSMaxY(rootFrame) - titlePadding - NSHeight(titleFrame);
299 312
300 // Set the message and recalculate the frame. 313 // Set the message and recalculate the frame.
301 [message_ setStringValue:base::SysUTF16ToNSString( 314 [message_ setStringValue:base::SysUTF16ToNSString(
302 [self wrapText:notification_->message() 315 [self wrapText:notification_->message()
303 forField:title_ 316 forField:message_
304 maxNumberOfLines:message_center::kMessageExpandedLineLimit])]; 317 maxNumberOfLines:message_center::kMessageExpandedLineLimit])];
305 [message_ setHidden:NO];
306 [message_ sizeToFit]; 318 [message_ sizeToFit];
307 NSRect messageFrame = [message_ frame]; 319 NSRect messageFrame = [message_ frame];
308 messageFrame.origin.y = 320
309 NSMinY(titleFrame) - messagePadding - NSHeight(messageFrame); 321 // If there are list items, then the message_ view should not be displayed.
310 messageFrame.size.height = NSHeight([message_ frame]); 322 const std::vector<message_center::NotificationItem>& items =
323 notification->items();
324 if (items.size() > 0) {
325 [message_ setHidden:YES];
326 messageFrame.origin.y = titleFrame.origin.y;
327 messageFrame.size.height = 0;
328 } else {
329 [message_ setHidden:NO];
330 messageFrame.origin.y =
331 NSMinY(titleFrame) - messagePadding - NSHeight(messageFrame);
332 messageFrame.size.height = NSHeight([message_ frame]);
333 }
334
335 // Set the context message and recalculate the frame.
336 [contextMessage_ setStringValue:base::SysUTF16ToNSString(
337 [self wrapText:notification_->context_message()
338 forField:contextMessage_
339 maxNumberOfLines:message_center::kContextMessageLineLimit])];
340 [contextMessage_ sizeToFit];
341 NSRect contextMessageFrame = [contextMessage_ frame];
342
343 if (notification_->context_message().empty()) {
344 [contextMessage_ setHidden:YES];
345 contextMessageFrame.origin.y = messageFrame.origin.y;
346 contextMessageFrame.size.height = 0;
347 } else {
348 [contextMessage_ setHidden:NO];
349 contextMessageFrame.origin.y =
350 NSMinY(messageFrame) -
351 contextMessagePadding -
352 NSHeight(contextMessageFrame);
353 contextMessageFrame.size.height = NSHeight([contextMessage_ frame]);
354 }
311 355
312 // Create the list item views (up to a maximum). 356 // Create the list item views (up to a maximum).
313 [listItemView_ removeFromSuperview]; 357 [listItemView_ removeFromSuperview];
314 const std::vector<message_center::NotificationItem>& items =
315 notification->items();
316 NSRect listFrame = NSZeroRect; 358 NSRect listFrame = NSZeroRect;
317 if (items.size() > 0) { 359 if (items.size() > 0) {
318 // If there are list items, then the message_ view should not be displayed.
319 [message_ setHidden:YES];
320 messageFrame.origin.y = titleFrame.origin.y;
321 messageFrame.size.height = 0;
322
323 listFrame = [self currentContentRect]; 360 listFrame = [self currentContentRect];
324 listFrame.origin.y = 0; 361 listFrame.origin.y = 0;
325 listFrame.size.height = 0; 362 listFrame.size.height = 0;
326 listItemView_.reset([[NSView alloc] initWithFrame:listFrame]); 363 listItemView_.reset([[NSView alloc] initWithFrame:listFrame]);
327 [listItemView_ accessibilitySetOverrideValue:NSAccessibilityListRole 364 [listItemView_ accessibilitySetOverrideValue:NSAccessibilityListRole
328 forAttribute:NSAccessibilityRoleAttribute]; 365 forAttribute:NSAccessibilityRoleAttribute];
329 [listItemView_ 366 [listItemView_
330 accessibilitySetOverrideValue:NSAccessibilityContentListSubrole 367 accessibilitySetOverrideValue:NSAccessibilityContentListSubrole
331 forAttribute:NSAccessibilitySubroleAttribute]; 368 forAttribute:NSAccessibilitySubroleAttribute];
332 CGFloat y = 0; 369 CGFloat y = 0;
333 370
334 NSFont* font = [NSFont systemFontOfSize:message_center::kMessageFontSize]; 371 NSFont* font = [NSFont systemFontOfSize:message_center::kMessageFontSize];
335 CGFloat lineHeight = roundf(NSHeight([font boundingRectForFont])); 372 CGFloat lineHeight = roundf(NSHeight([font boundingRectForFont]));
336 373
337 const int kNumNotifications = 374 const int kNumNotifications =
338 std::min(items.size(), message_center::kNotificationMaximumItems); 375 std::min(items.size(), message_center::kNotificationMaximumItems);
339 for (int i = kNumNotifications - 1; i >= 0; --i) { 376 for (int i = kNumNotifications - 1; i >= 0; --i) {
340 NSTextField* field = [self newLabelWithFrame: 377 NSTextField* field = [self newLabelWithFrame:
341 NSMakeRect(0, y, NSWidth(listFrame), lineHeight)]; 378 NSMakeRect(0, y, NSWidth(listFrame), lineHeight)];
342 [[field cell] setUsesSingleLineMode:YES]; 379 [[field cell] setUsesSingleLineMode:YES];
343 [field setAttributedStringValue: 380 [field setAttributedStringValue:
344 [MCNotificationController attributedStringForItem:items[i] 381 [MCNotificationController attributedStringForItem:items[i]
345 font:font]]; 382 font:font]];
346 [listItemView_ addSubview:field]; 383 [listItemView_ addSubview:field];
347 y += lineHeight; 384 y += lineHeight;
348 } 385 }
349 // TODO(thakis): The spacing is not completely right. 386 // TODO(thakis): The spacing is not completely right.
350 CGFloat listTopPadding = 387 CGFloat listTopPadding =
351 message_center::kTextTopPadding - messageTopGap; 388 message_center::kTextTopPadding - contextMessageTopGap;
352 listFrame.size.height = y; 389 listFrame.size.height = y;
353 listFrame.origin.y = 390 listFrame.origin.y =
354 NSMinY(titleFrame) - listTopPadding - NSHeight(listFrame); 391 NSMinY(contextMessageFrame) - listTopPadding - NSHeight(listFrame);
355 [listItemView_ setFrame:listFrame]; 392 [listItemView_ setFrame:listFrame];
356 [[self view] addSubview:listItemView_]; 393 [[self view] addSubview:listItemView_];
357 } 394 }
358 395
359 // Create the progress bar view if needed. 396 // Create the progress bar view if needed.
360 [progressBarView_ removeFromSuperview]; 397 [progressBarView_ removeFromSuperview];
361 NSRect progressBarFrame = NSZeroRect; 398 NSRect progressBarFrame = NSZeroRect;
362 if (notification->type() == message_center::NOTIFICATION_TYPE_PROGRESS) { 399 if (notification->type() == message_center::NOTIFICATION_TYPE_PROGRESS) {
363 progressBarFrame = [self currentContentRect]; 400 progressBarFrame = [self currentContentRect];
364 progressBarFrame.origin.y = NSMinY(messageFrame) - 401 progressBarFrame.origin.y = NSMinY(contextMessageFrame) -
365 message_center::kProgressBarTopPadding - 402 message_center::kProgressBarTopPadding -
366 message_center::kProgressBarThickness; 403 message_center::kProgressBarThickness;
367 progressBarFrame.size.height = message_center::kProgressBarThickness; 404 progressBarFrame.size.height = message_center::kProgressBarThickness;
368 progressBarView_.reset( 405 progressBarView_.reset(
369 [[MCNotificationProgressBar alloc] initWithFrame:progressBarFrame]); 406 [[MCNotificationProgressBar alloc] initWithFrame:progressBarFrame]);
370 // Setting indeterminate to NO does not work with custom drawRect. 407 // Setting indeterminate to NO does not work with custom drawRect.
371 [progressBarView_ setIndeterminate:YES]; 408 [progressBarView_ setIndeterminate:YES];
372 [progressBarView_ setStyle:NSProgressIndicatorBarStyle]; 409 [progressBarView_ setStyle:NSProgressIndicatorBarStyle];
373 [progressBarView_ setDoubleValue:notification->progress()]; 410 [progressBarView_ setDoubleValue:notification->progress()];
374 [[self view] addSubview:progressBarView_]; 411 [[self view] addSubview:progressBarView_];
375 } 412 }
376 413
377 // If the bottom-most element so far is out of the rootView's bounds, resize 414 // If the bottom-most element so far is out of the rootView's bounds, resize
378 // the view. 415 // the view.
379 CGFloat minY = NSMinY(messageFrame); 416 CGFloat minY = NSMinY(contextMessageFrame);
380 if (listItemView_ && NSMinY(listFrame) < minY) 417 if (listItemView_ && NSMinY(listFrame) < minY)
381 minY = NSMinY(listFrame); 418 minY = NSMinY(listFrame);
382 if (progressBarView_ && NSMinY(progressBarFrame) < minY) 419 if (progressBarView_ && NSMinY(progressBarFrame) < minY)
383 minY = NSMinY(progressBarFrame); 420 minY = NSMinY(progressBarFrame);
384 if (minY < messagePadding) { 421 if (minY < messagePadding) {
385 CGFloat delta = messagePadding - minY; 422 CGFloat delta = messagePadding - minY;
386 rootFrame.size.height += delta; 423 rootFrame.size.height += delta;
387 titleFrame.origin.y += delta; 424 titleFrame.origin.y += delta;
388 messageFrame.origin.y += delta; 425 messageFrame.origin.y += delta;
426 contextMessageFrame.origin.y += delta;
389 listFrame.origin.y += delta; 427 listFrame.origin.y += delta;
390 progressBarFrame.origin.y += delta; 428 progressBarFrame.origin.y += delta;
391 } 429 }
392 430
393 // Add the bottom container view. 431 // Add the bottom container view.
394 NSRect frame = rootFrame; 432 NSRect frame = rootFrame;
395 frame.size.height = 0; 433 frame.size.height = 0;
396 [bottomView_ removeFromSuperview]; 434 [bottomView_ removeFromSuperview];
397 bottomView_.reset([[NSView alloc] initWithFrame:frame]); 435 bottomView_.reset([[NSView alloc] initWithFrame:frame]);
398 CGFloat y = 0; 436 CGFloat y = 0;
(...skipping 50 matching lines...) Expand 10 before | Expand all | Expand 10 after
449 frame.size.height += NSHeight(imageFrame); 487 frame.size.height += NSHeight(imageFrame);
450 [bottomView_ addSubview:imageView]; 488 [bottomView_ addSubview:imageView];
451 } 489 }
452 490
453 [bottomView_ setFrame:frame]; 491 [bottomView_ setFrame:frame];
454 [[self view] addSubview:bottomView_]; 492 [[self view] addSubview:bottomView_];
455 493
456 rootFrame.size.height += NSHeight(frame); 494 rootFrame.size.height += NSHeight(frame);
457 titleFrame.origin.y += NSHeight(frame); 495 titleFrame.origin.y += NSHeight(frame);
458 messageFrame.origin.y += NSHeight(frame); 496 messageFrame.origin.y += NSHeight(frame);
497 contextMessageFrame.origin.y += NSHeight(frame);
459 listFrame.origin.y += NSHeight(frame); 498 listFrame.origin.y += NSHeight(frame);
460 progressBarFrame.origin.y += NSHeight(frame); 499 progressBarFrame.origin.y += NSHeight(frame);
461 500
462 // Make sure that there is a minimum amount of spacing below the icon and 501 // Make sure that there is a minimum amount of spacing below the icon and
463 // the edge of the frame. 502 // the edge of the frame.
464 CGFloat bottomDelta = NSHeight(rootFrame) - NSHeight([icon_ frame]); 503 CGFloat bottomDelta = NSHeight(rootFrame) - NSHeight([icon_ frame]);
465 if (bottomDelta > 0 && bottomDelta < message_center::kIconBottomPadding) { 504 if (bottomDelta > 0 && bottomDelta < message_center::kIconBottomPadding) {
466 CGFloat bottomAdjust = message_center::kIconBottomPadding - bottomDelta; 505 CGFloat bottomAdjust = message_center::kIconBottomPadding - bottomDelta;
467 rootFrame.size.height += bottomAdjust; 506 rootFrame.size.height += bottomAdjust;
468 titleFrame.origin.y += bottomAdjust; 507 titleFrame.origin.y += bottomAdjust;
469 messageFrame.origin.y += bottomAdjust; 508 messageFrame.origin.y += bottomAdjust;
509 contextMessageFrame.origin.y += bottomAdjust;
470 listFrame.origin.y += bottomAdjust; 510 listFrame.origin.y += bottomAdjust;
471 progressBarFrame.origin.y += bottomAdjust; 511 progressBarFrame.origin.y += bottomAdjust;
472 } 512 }
473 513
474 [[self view] setFrame:rootFrame]; 514 [[self view] setFrame:rootFrame];
475 [title_ setFrame:titleFrame]; 515 [title_ setFrame:titleFrame];
476 [message_ setFrame:messageFrame]; 516 [message_ setFrame:messageFrame];
517 [contextMessage_ setFrame:contextMessageFrame];
477 [listItemView_ setFrame:listFrame]; 518 [listItemView_ setFrame:listFrame];
478 [progressBarView_ setFrame:progressBarFrame]; 519 [progressBarView_ setFrame:progressBarFrame];
479 520
480 return rootFrame; 521 return rootFrame;
481 } 522 }
482 523
483 - (void)close:(id)sender { 524 - (void)close:(id)sender {
484 [closeButton_ setTarget:nil]; 525 [closeButton_ setTarget:nil];
485 messageCenter_->RemoveNotification([self notificationID], /*by_user=*/true); 526 messageCenter_->RemoveNotification([self notificationID], /*by_user=*/true);
486 } 527 }
(...skipping 117 matching lines...) Expand 10 before | Expand all | Expand 10 after
604 message_center::kRegularTextColor)]; 645 message_center::kRegularTextColor)];
605 [title_ setFont:[NSFont messageFontOfSize:message_center::kTitleFontSize]]; 646 [title_ setFont:[NSFont messageFontOfSize:message_center::kTitleFontSize]];
606 } 647 }
607 648
608 - (void)configureBodyInFrame:(NSRect)rootFrame { 649 - (void)configureBodyInFrame:(NSRect)rootFrame {
609 NSRect frame = [self currentContentRect]; 650 NSRect frame = [self currentContentRect];
610 frame.size.height = 0; 651 frame.size.height = 0;
611 message_.reset([self newLabelWithFrame:frame]); 652 message_.reset([self newLabelWithFrame:frame]);
612 [message_ setAutoresizingMask:NSViewMinYMargin]; 653 [message_ setAutoresizingMask:NSViewMinYMargin];
613 [message_ setTextColor:gfx::SkColorToCalibratedNSColor( 654 [message_ setTextColor:gfx::SkColorToCalibratedNSColor(
655 message_center::kRegularTextColor)];
656 [message_ setFont:
657 [NSFont messageFontOfSize:message_center::kMessageFontSize]];
658 }
659
660 - (void)configureContextMessageInFrame:(NSRect)rootFrame {
661 NSRect frame = [self currentContentRect];
662 frame.size.height = 0;
663 contextMessage_.reset([self newLabelWithFrame:frame]);
664 [contextMessage_ setAutoresizingMask:NSViewMinYMargin];
665 [contextMessage_ setTextColor:gfx::SkColorToCalibratedNSColor(
614 message_center::kDimTextColor)]; 666 message_center::kDimTextColor)];
615 [message_ setFont: 667 [contextMessage_ setFont:
616 [NSFont messageFontOfSize:message_center::kMessageFontSize]]; 668 [NSFont messageFontOfSize:message_center::kMessageFontSize]];
617 } 669 }
618 670
619 - (NSTextField*)newLabelWithFrame:(NSRect)frame { 671 - (NSTextField*)newLabelWithFrame:(NSRect)frame {
620 NSTextField* label = [[NSTextField alloc] initWithFrame:frame]; 672 NSTextField* label = [[NSTextField alloc] initWithFrame:frame];
621 [label setDrawsBackground:NO]; 673 [label setDrawsBackground:NO];
622 [label setBezeled:NO]; 674 [label setBezeled:NO];
623 [label setEditable:NO]; 675 [label setEditable:NO];
624 [label setSelectable:NO]; 676 [label setSelectable:NO];
625 return label; 677 return label;
(...skipping 29 matching lines...) Expand all
655 if (font.GetStringWidth(last) > width) 707 if (font.GetStringWidth(last) > width)
656 last = ui::ElideText(last, font, width, ui::ELIDE_AT_END); 708 last = ui::ElideText(last, font, width, ui::ELIDE_AT_END);
657 wrapped.resize(lines - 1); 709 wrapped.resize(lines - 1);
658 wrapped.push_back(last); 710 wrapped.push_back(last);
659 } 711 }
660 712
661 return JoinString(wrapped, '\n'); 713 return JoinString(wrapped, '\n');
662 } 714 }
663 715
664 @end 716 @end
OLDNEW
« no previous file with comments | « ui/message_center/cocoa/notification_controller.h ('k') | ui/message_center/cocoa/notification_controller_unittest.mm » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698