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

Side by Side Diff: chrome/browser/ui/views/desktop_capture/desktop_media_picker_views.cc

Issue 1978633002: Desktop Capture Picker New UI: Split File (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Unittest 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 2013 The Chromium Authors. All rights reserved. 1 // Copyright 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 #include "chrome/browser/ui/views/desktop_capture/desktop_media_picker_views.h" 5 #include "chrome/browser/ui/views/desktop_capture/desktop_media_picker_views.h"
6 6
7 #include <stddef.h>
8 #include <utility>
9
10 #include "base/callback.h" 7 #include "base/callback.h"
11 #include "base/command_line.h" 8 #include "base/command_line.h"
12 #include "base/strings/utf_string_conversions.h"
13 #include "build/build_config.h"
14 #include "chrome/browser/media/combined_desktop_media_list.h"
15 #include "chrome/browser/media/desktop_media_list.h" 9 #include "chrome/browser/media/desktop_media_list.h"
10 #include "chrome/browser/ui/views/desktop_capture/desktop_media_list_view.h"
11 #include "chrome/browser/ui/views/desktop_capture/desktop_media_source_view.h"
16 #include "chrome/browser/ui/views/desktop_media_picker_views_deprecated.h" 12 #include "chrome/browser/ui/views/desktop_media_picker_views_deprecated.h"
17 #include "chrome/common/chrome_switches.h"
18 #include "chrome/grit/chromium_strings.h" 13 #include "chrome/grit/chromium_strings.h"
19 #include "chrome/grit/generated_resources.h" 14 #include "chrome/grit/generated_resources.h"
20 #include "chrome/grit/google_chrome_strings.h" 15 #include "chrome/grit/google_chrome_strings.h"
21 #include "components/constrained_window/constrained_window_views.h" 16 #include "components/constrained_window/constrained_window_views.h"
22 #include "components/web_modal/web_contents_modal_dialog_manager.h" 17 #include "components/web_modal/web_contents_modal_dialog_manager.h"
23 #include "content/public/browser/browser_thread.h" 18 #include "content/public/browser/browser_thread.h"
24 #include "content/public/browser/render_frame_host.h" 19 #include "content/public/browser/render_frame_host.h"
25 #include "content/public/browser/web_contents_delegate.h" 20 #include "content/public/browser/web_contents_delegate.h"
26 #include "extensions/common/switches.h" 21 #include "extensions/common/switches.h"
27 #include "grit/components_strings.h" 22 #include "grit/components_strings.h"
28 #include "ui/aura/window_tree_host.h" 23 #include "ui/aura/window_tree_host.h"
29 #include "ui/base/l10n/l10n_util.h" 24 #include "ui/base/l10n/l10n_util.h"
30 #include "ui/events/event_constants.h"
31 #include "ui/events/keycodes/keyboard_codes.h" 25 #include "ui/events/keycodes/keyboard_codes.h"
32 #include "ui/gfx/canvas.h" 26 #include "ui/gfx/canvas.h"
33 #include "ui/native_theme/native_theme.h"
34 #include "ui/views/background.h"
35 #include "ui/views/bubble/bubble_frame_view.h"
36 #include "ui/views/controls/button/checkbox.h" 27 #include "ui/views/controls/button/checkbox.h"
37 #include "ui/views/controls/image_view.h"
38 #include "ui/views/controls/label.h"
39 #include "ui/views/controls/scroll_view.h" 28 #include "ui/views/controls/scroll_view.h"
40 #include "ui/views/controls/tabbed_pane/tabbed_pane.h" 29 #include "ui/views/controls/tabbed_pane/tabbed_pane.h"
41 #include "ui/views/layout/box_layout.h" 30 #include "ui/views/layout/box_layout.h"
42 #include "ui/views/layout/layout_constants.h" 31 #include "ui/views/layout/layout_constants.h"
43 #include "ui/views/widget/widget.h" 32 #include "ui/views/widget/widget.h"
44 #include "ui/views/window/dialog_client_view.h" 33 #include "ui/views/window/dialog_client_view.h"
45 #include "ui/wm/core/shadow_types.h" 34 #include "ui/wm/core/shadow_types.h"
46 35
47 using content::DesktopMediaID; 36 using content::DesktopMediaID;
48 37
49 namespace { 38 namespace {
50 39
51 const int kThumbnailWidth = 160;
52 const int kThumbnailHeight = 100;
53 const int kThumbnailMargin = 10;
54 const int kLabelHeight = 40;
55 const int kListItemWidth = kThumbnailMargin * 2 + kThumbnailWidth;
56 const int kListItemHeight =
57 kThumbnailMargin * 2 + kThumbnailHeight + kLabelHeight;
58 const int kListColumns = 3;
59 const int kTotalListWidth = kListColumns * kListItemWidth;
60
61 const int kDesktopMediaSourceViewGroupId = 1;
62
63 const char kDesktopMediaSourceViewClassName[] =
64 "DesktopMediaPicker_DesktopMediaSourceView";
65
66 #if !defined(USE_ASH) 40 #if !defined(USE_ASH)
67 DesktopMediaID::Id AcceleratedWidgetToDesktopMediaId( 41 DesktopMediaID::Id AcceleratedWidgetToDesktopMediaId(
68 gfx::AcceleratedWidget accelerated_widget) { 42 gfx::AcceleratedWidget accelerated_widget) {
69 #if defined(OS_WIN) 43 #if defined(OS_WIN)
70 return reinterpret_cast<DesktopMediaID::Id>(accelerated_widget); 44 return reinterpret_cast<DesktopMediaID::Id>(accelerated_widget);
71 #else 45 #else
72 return static_cast<DesktopMediaID::Id>(accelerated_widget); 46 return static_cast<DesktopMediaID::Id>(accelerated_widget);
73 #endif 47 #endif
74 } 48 }
75 #endif 49 #endif
76 50
77 int GetMediaListViewHeightForRows(size_t rows) {
78 return kListItemHeight * rows;
79 }
80
81 } // namespace 51 } // namespace
82 52
83 DesktopMediaSourceView::DesktopMediaSourceView(DesktopMediaListView* parent,
84 DesktopMediaID source_id)
85 : parent_(parent),
86 source_id_(source_id),
87 image_view_(new views::ImageView()),
88 label_(new views::Label()),
89 selected_(false) {
90 AddChildView(image_view_);
91 AddChildView(label_);
92 SetFocusBehavior(FocusBehavior::ALWAYS);
93 }
94
95 DesktopMediaSourceView::~DesktopMediaSourceView() {}
96
97 void DesktopMediaSourceView::SetName(const base::string16& name) {
98 label_->SetText(name);
99 }
100
101 void DesktopMediaSourceView::SetThumbnail(const gfx::ImageSkia& thumbnail) {
102 image_view_->SetImage(thumbnail);
103 }
104
105 void DesktopMediaSourceView::SetSelected(bool selected) {
106 if (selected == selected_)
107 return;
108 selected_ = selected;
109
110 if (selected) {
111 // Unselect all other sources.
112 Views neighbours;
113 parent()->GetViewsInGroup(GetGroup(), &neighbours);
114 for (Views::iterator i(neighbours.begin()); i != neighbours.end(); ++i) {
115 if (*i != this) {
116 DCHECK_EQ((*i)->GetClassName(), kDesktopMediaSourceViewClassName);
117 DesktopMediaSourceView* source_view =
118 static_cast<DesktopMediaSourceView*>(*i);
119 source_view->SetSelected(false);
120 }
121 }
122
123 const SkColor bg_color = GetNativeTheme()->GetSystemColor(
124 ui::NativeTheme::kColorId_FocusedMenuItemBackgroundColor);
125 set_background(views::Background::CreateSolidBackground(bg_color));
126
127 parent_->OnSelectionChanged();
128 } else {
129 set_background(NULL);
130 }
131
132 SchedulePaint();
133 }
134
135 const char* DesktopMediaSourceView::GetClassName() const {
136 return kDesktopMediaSourceViewClassName;
137 }
138
139 void DesktopMediaSourceView::Layout() {
140 image_view_->SetBounds(kThumbnailMargin, kThumbnailMargin, kThumbnailWidth,
141 kThumbnailHeight);
142 label_->SetBounds(kThumbnailMargin, kThumbnailHeight + kThumbnailMargin,
143 kThumbnailWidth, kLabelHeight);
144 }
145
146 views::View* DesktopMediaSourceView::GetSelectedViewForGroup(int group) {
147 Views neighbours;
148 parent()->GetViewsInGroup(group, &neighbours);
149 if (neighbours.empty())
150 return NULL;
151
152 for (Views::iterator i(neighbours.begin()); i != neighbours.end(); ++i) {
153 DCHECK_EQ((*i)->GetClassName(), kDesktopMediaSourceViewClassName);
154 DesktopMediaSourceView* source_view =
155 static_cast<DesktopMediaSourceView*>(*i);
156 if (source_view->selected_)
157 return source_view;
158 }
159 return NULL;
160 }
161
162 bool DesktopMediaSourceView::IsGroupFocusTraversable() const {
163 return false;
164 }
165
166 void DesktopMediaSourceView::OnPaint(gfx::Canvas* canvas) {
167 View::OnPaint(canvas);
168 if (HasFocus()) {
169 gfx::Rect bounds(GetLocalBounds());
170 bounds.Inset(kThumbnailMargin / 2, kThumbnailMargin / 2);
171 canvas->DrawFocusRect(bounds);
172 }
173 }
174
175 void DesktopMediaSourceView::OnFocus() {
176 View::OnFocus();
177 SetSelected(true);
178 ScrollRectToVisible(gfx::Rect(size()));
179 // We paint differently when focused.
180 SchedulePaint();
181 }
182
183 void DesktopMediaSourceView::OnBlur() {
184 View::OnBlur();
185 // We paint differently when focused.
186 SchedulePaint();
187 }
188
189 bool DesktopMediaSourceView::OnMousePressed(const ui::MouseEvent& event) {
190 if (event.GetClickCount() == 1) {
191 RequestFocus();
192 } else if (event.GetClickCount() == 2) {
193 RequestFocus();
194 parent_->OnDoubleClick();
195 }
196 return true;
197 }
198
199 void DesktopMediaSourceView::OnGestureEvent(ui::GestureEvent* event) {
200 if (event->type() == ui::ET_GESTURE_TAP &&
201 event->details().tap_count() == 2) {
202 RequestFocus();
203 parent_->OnDoubleClick();
204 event->SetHandled();
205 return;
206 }
207
208 // Detect tap gesture using ET_GESTURE_TAP_DOWN so the view also gets focused
209 // on the long tap (when the tap gesture starts).
210 if (event->type() == ui::ET_GESTURE_TAP_DOWN) {
211 RequestFocus();
212 event->SetHandled();
213 }
214 }
215
216 DesktopMediaListView::DesktopMediaListView(
217 DesktopMediaPickerDialogView* parent,
218 std::unique_ptr<DesktopMediaList> media_list)
219 : parent_(parent), media_list_(std::move(media_list)), weak_factory_(this) {
220 media_list_->SetThumbnailSize(gfx::Size(kThumbnailWidth, kThumbnailHeight));
221 SetFocusBehavior(FocusBehavior::ALWAYS);
222 }
223
224 DesktopMediaListView::~DesktopMediaListView() {}
225
226 void DesktopMediaListView::StartUpdating(DesktopMediaID dialog_window_id) {
227 media_list_->SetViewDialogWindowId(dialog_window_id);
228 media_list_->StartUpdating(this);
229 }
230
231 void DesktopMediaListView::OnSelectionChanged() {
232 parent_->OnSelectionChanged();
233 }
234
235 void DesktopMediaListView::OnDoubleClick() {
236 parent_->OnDoubleClick();
237 }
238
239 DesktopMediaSourceView* DesktopMediaListView::GetSelection() {
240 for (int i = 0; i < child_count(); ++i) {
241 DesktopMediaSourceView* source_view =
242 static_cast<DesktopMediaSourceView*>(child_at(i));
243 DCHECK_EQ(source_view->GetClassName(), kDesktopMediaSourceViewClassName);
244 if (source_view->is_selected())
245 return source_view;
246 }
247 return NULL;
248 }
249
250 gfx::Size DesktopMediaListView::GetPreferredSize() const {
251 int total_rows = (child_count() + kListColumns - 1) / kListColumns;
252 return gfx::Size(kTotalListWidth,
253 GetMediaListViewHeightForRows(total_rows));
254 }
255
256 void DesktopMediaListView::Layout() {
257 int x = 0;
258 int y = 0;
259
260 for (int i = 0; i < child_count(); ++i) {
261 if (x + kListItemWidth > kTotalListWidth) {
262 x = 0;
263 y += kListItemHeight;
264 }
265
266 View* source_view = child_at(i);
267 source_view->SetBounds(x, y, kListItemWidth, kListItemHeight);
268
269 x += kListItemWidth;
270 }
271
272 y += kListItemHeight;
273 }
274
275 bool DesktopMediaListView::OnKeyPressed(const ui::KeyEvent& event) {
276 int position_increment = 0;
277 switch (event.key_code()) {
278 case ui::VKEY_UP:
279 position_increment = -kListColumns;
280 break;
281 case ui::VKEY_DOWN:
282 position_increment = kListColumns;
283 break;
284 case ui::VKEY_LEFT:
285 position_increment = -1;
286 break;
287 case ui::VKEY_RIGHT:
288 position_increment = 1;
289 break;
290 default:
291 return false;
292 }
293
294 if (position_increment != 0) {
295 DesktopMediaSourceView* selected = GetSelection();
296 DesktopMediaSourceView* new_selected = NULL;
297
298 if (selected) {
299 int index = GetIndexOf(selected);
300 int new_index = index + position_increment;
301 if (new_index >= child_count())
302 new_index = child_count() - 1;
303 else if (new_index < 0)
304 new_index = 0;
305 if (index != new_index) {
306 new_selected =
307 static_cast<DesktopMediaSourceView*>(child_at(new_index));
308 }
309 } else if (has_children()) {
310 new_selected = static_cast<DesktopMediaSourceView*>(child_at(0));
311 }
312
313 if (new_selected) {
314 GetFocusManager()->SetFocusedView(new_selected);
315 }
316
317 return true;
318 }
319
320 return false;
321 }
322
323 void DesktopMediaListView::OnSourceAdded(DesktopMediaList* list, int index) {
324 const DesktopMediaList::Source& source = media_list_->GetSource(index);
325 DesktopMediaSourceView* source_view =
326 new DesktopMediaSourceView(this, source.id);
327 source_view->SetName(source.name);
328 source_view->SetGroup(kDesktopMediaSourceViewGroupId);
329 AddChildViewAt(source_view, index);
330
331 PreferredSizeChanged();
332
333 if (child_count() % kListColumns == 1)
334 parent_->OnMediaListRowsChanged();
335
336 // Auto select the first screen.
337 if (index == 0 && source.id.type == DesktopMediaID::TYPE_SCREEN)
338 source_view->RequestFocus();
339
340 std::string autoselect_source =
341 base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
342 switches::kAutoSelectDesktopCaptureSource);
343 if (!autoselect_source.empty() &&
344 base::ASCIIToUTF16(autoselect_source) == source.name) {
345 // Select, then accept and close the dialog when we're done adding sources.
346 source_view->OnFocus();
347 content::BrowserThread::PostTask(
348 content::BrowserThread::UI, FROM_HERE,
349 base::Bind(&DesktopMediaListView::AcceptSelection,
350 weak_factory_.GetWeakPtr()));
351 }
352 }
353
354 void DesktopMediaListView::OnSourceRemoved(DesktopMediaList* list, int index) {
355 DesktopMediaSourceView* view =
356 static_cast<DesktopMediaSourceView*>(child_at(index));
357 DCHECK(view);
358 DCHECK_EQ(view->GetClassName(), kDesktopMediaSourceViewClassName);
359 bool was_selected = view->is_selected();
360 RemoveChildView(view);
361 delete view;
362
363 if (was_selected)
364 OnSelectionChanged();
365
366 PreferredSizeChanged();
367
368 if (child_count() % kListColumns == 0)
369 parent_->OnMediaListRowsChanged();
370 }
371
372 void DesktopMediaListView::OnSourceMoved(DesktopMediaList* list,
373 int old_index,
374 int new_index) {
375 DesktopMediaSourceView* view =
376 static_cast<DesktopMediaSourceView*>(child_at(old_index));
377 ReorderChildView(view, new_index);
378 PreferredSizeChanged();
379 }
380
381 void DesktopMediaListView::OnSourceNameChanged(DesktopMediaList* list,
382 int index) {
383 const DesktopMediaList::Source& source = media_list_->GetSource(index);
384 DesktopMediaSourceView* source_view =
385 static_cast<DesktopMediaSourceView*>(child_at(index));
386 source_view->SetName(source.name);
387 }
388
389 void DesktopMediaListView::OnSourceThumbnailChanged(DesktopMediaList* list,
390 int index) {
391 const DesktopMediaList::Source& source = media_list_->GetSource(index);
392 DesktopMediaSourceView* source_view =
393 static_cast<DesktopMediaSourceView*>(child_at(index));
394 source_view->SetThumbnail(source.thumbnail);
395 }
396
397 void DesktopMediaListView::AcceptSelection() {
398 OnSelectionChanged();
399 OnDoubleClick();
400 }
401
402 DesktopMediaPickerDialogView::DesktopMediaPickerDialogView( 53 DesktopMediaPickerDialogView::DesktopMediaPickerDialogView(
403 content::WebContents* parent_web_contents, 54 content::WebContents* parent_web_contents,
404 gfx::NativeWindow context, 55 gfx::NativeWindow context,
405 DesktopMediaPickerViews* parent, 56 DesktopMediaPickerViews* parent,
406 const base::string16& app_name, 57 const base::string16& app_name,
407 const base::string16& target_name, 58 const base::string16& target_name,
408 std::unique_ptr<DesktopMediaList> screen_list, 59 std::unique_ptr<DesktopMediaList> screen_list,
409 std::unique_ptr<DesktopMediaList> window_list, 60 std::unique_ptr<DesktopMediaList> window_list,
410 std::unique_ptr<DesktopMediaList> tab_list, 61 std::unique_ptr<DesktopMediaList> tab_list,
411 bool request_audio) 62 bool request_audio)
(...skipping 11 matching lines...) Expand all
423 74
424 if (screen_list) { 75 if (screen_list) {
425 source_types_.push_back(DesktopMediaID::TYPE_SCREEN); 76 source_types_.push_back(DesktopMediaID::TYPE_SCREEN);
426 77
427 views::ScrollView* screen_scroll_view = 78 views::ScrollView* screen_scroll_view =
428 views::ScrollView::CreateScrollViewWithBorder(); 79 views::ScrollView::CreateScrollViewWithBorder();
429 list_views_.push_back( 80 list_views_.push_back(
430 new DesktopMediaListView(this, std::move(screen_list))); 81 new DesktopMediaListView(this, std::move(screen_list)));
431 82
432 screen_scroll_view->SetContents(list_views_.back()); 83 screen_scroll_view->SetContents(list_views_.back());
433 screen_scroll_view->ClipHeightTo(GetMediaListViewHeightForRows(1), 84 screen_scroll_view->ClipHeightTo(kListItemHeight, kListItemHeight * 2);
434 GetMediaListViewHeightForRows(2));
435 pane_->AddTab( 85 pane_->AddTab(
436 l10n_util::GetStringUTF16(IDS_DESKTOP_MEDIA_PICKER_SOURCE_TYPE_SCREEN), 86 l10n_util::GetStringUTF16(IDS_DESKTOP_MEDIA_PICKER_SOURCE_TYPE_SCREEN),
437 screen_scroll_view); 87 screen_scroll_view);
438 pane_->set_listener(this); 88 pane_->set_listener(this);
439 } 89 }
440 90
441 if (window_list) { 91 if (window_list) {
442 source_types_.push_back(DesktopMediaID::TYPE_WINDOW); 92 source_types_.push_back(DesktopMediaID::TYPE_WINDOW);
443 views::ScrollView* window_scroll_view = 93 views::ScrollView* window_scroll_view =
444 views::ScrollView::CreateScrollViewWithBorder(); 94 views::ScrollView::CreateScrollViewWithBorder();
445 list_views_.push_back( 95 list_views_.push_back(
446 new DesktopMediaListView(this, std::move(window_list))); 96 new DesktopMediaListView(this, std::move(window_list)));
447 97
448 window_scroll_view->SetContents(list_views_.back()); 98 window_scroll_view->SetContents(list_views_.back());
449 window_scroll_view->ClipHeightTo(GetMediaListViewHeightForRows(1), 99 window_scroll_view->ClipHeightTo(kListItemHeight, kListItemHeight * 2);
450 GetMediaListViewHeightForRows(2));
451 100
452 pane_->AddTab( 101 pane_->AddTab(
453 l10n_util::GetStringUTF16(IDS_DESKTOP_MEDIA_PICKER_SOURCE_TYPE_WINDOW), 102 l10n_util::GetStringUTF16(IDS_DESKTOP_MEDIA_PICKER_SOURCE_TYPE_WINDOW),
454 window_scroll_view); 103 window_scroll_view);
455 pane_->set_listener(this); 104 pane_->set_listener(this);
456 } 105 }
457 106
458 if (tab_list) { 107 if (tab_list) {
459 source_types_.push_back(DesktopMediaID::TYPE_WEB_CONTENTS); 108 source_types_.push_back(DesktopMediaID::TYPE_WEB_CONTENTS);
460 views::ScrollView* tab_scroll_view = 109 views::ScrollView* tab_scroll_view =
461 views::ScrollView::CreateScrollViewWithBorder(); 110 views::ScrollView::CreateScrollViewWithBorder();
462 list_views_.push_back(new DesktopMediaListView(this, std::move(tab_list))); 111 list_views_.push_back(new DesktopMediaListView(this, std::move(tab_list)));
463 112
464 tab_scroll_view->SetContents(list_views_.back()); 113 tab_scroll_view->SetContents(list_views_.back());
465 tab_scroll_view->ClipHeightTo(GetMediaListViewHeightForRows(1), 114 tab_scroll_view->ClipHeightTo(kListItemHeight, kListItemHeight * 2);
466 GetMediaListViewHeightForRows(2));
467 115
468 pane_->AddTab( 116 pane_->AddTab(
469 l10n_util::GetStringUTF16(IDS_DESKTOP_MEDIA_PICKER_SOURCE_TYPE_TAB), 117 l10n_util::GetStringUTF16(IDS_DESKTOP_MEDIA_PICKER_SOURCE_TYPE_TAB),
470 tab_scroll_view); 118 tab_scroll_view);
471 pane_->set_listener(this); 119 pane_->set_listener(this);
472 } 120 }
473 121
474 if (app_name == target_name) { 122 if (app_name == target_name) {
475 description_label_->SetText( 123 description_label_->SetText(
476 l10n_util::GetStringFUTF16(IDS_DESKTOP_MEDIA_PICKER_TEXT, app_name)); 124 l10n_util::GetStringFUTF16(IDS_DESKTOP_MEDIA_PICKER_TEXT, app_name));
(...skipping 250 matching lines...) Expand 10 before | Expand all | Expand 10 after
727 375
728 // static 376 // static
729 std::unique_ptr<DesktopMediaPicker> DesktopMediaPicker::Create() { 377 std::unique_ptr<DesktopMediaPicker> DesktopMediaPicker::Create() {
730 if (!base::CommandLine::ForCurrentProcess()->HasSwitch( 378 if (!base::CommandLine::ForCurrentProcess()->HasSwitch(
731 extensions::switches::kDisableDesktopCapturePickerOldUI)) { 379 extensions::switches::kDisableDesktopCapturePickerOldUI)) {
732 return std::unique_ptr<DesktopMediaPicker>( 380 return std::unique_ptr<DesktopMediaPicker>(
733 new deprecated::DesktopMediaPickerViews()); 381 new deprecated::DesktopMediaPickerViews());
734 } 382 }
735 return std::unique_ptr<DesktopMediaPicker>(new DesktopMediaPickerViews()); 383 return std::unique_ptr<DesktopMediaPicker>(new DesktopMediaPickerViews());
736 } 384 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698