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 3 // found in the LICENSE file. 4 5 #include "chrome/browser/ui/views/desktop_media_picker_views.h" 6 7 #include "base/callback.h" 8 #include "base/command_line.h" 9 #include "base/strings/utf_string_conversions.h" 10 #include "chrome/browser/media/desktop_media_list.h" 11 #include "chrome/browser/ui/ash/ash_util.h" 12 #include "chrome/browser/ui/views/constrained_window_views.h" 13 #include "chrome/common/chrome_switches.h" 14 #include "chrome/grit/generated_resources.h" 15 #include "components/web_modal/popup_manager.h" 16 #include "components/web_modal/web_contents_modal_dialog_manager.h" 17 #include "content/public/browser/browser_thread.h" 18 #include "content/public/browser/web_contents_delegate.h" 19 #include "ui/aura/window_tree_host.h" 20 #include "ui/base/l10n/l10n_util.h" 21 #include "ui/events/event_constants.h" 22 #include "ui/events/keycodes/keyboard_codes.h" 23 #include "ui/gfx/canvas.h" 24 #include "ui/native_theme/native_theme.h" 25 #include "ui/views/background.h" 26 #include "ui/views/bubble/bubble_frame_view.h" 27 #include "ui/views/controls/image_view.h" 28 #include "ui/views/controls/label.h" 29 #include "ui/views/controls/scroll_view.h" 30 #include "ui/views/layout/layout_constants.h" 31 #include "ui/views/widget/widget.h" 32 #include "ui/views/window/dialog_client_view.h" 33 #include "ui/wm/core/shadow_types.h" 34 35 using content::DesktopMediaID; 36 37 namespace { 38 39 const int kThumbnailWidth = 160; 40 const int kThumbnailHeight = 100; 41 const int kThumbnailMargin = 10; 42 const int kLabelHeight = 40; 43 const int kListItemWidth = kThumbnailMargin * 2 + kThumbnailWidth; 44 const int kListItemHeight = 45 kThumbnailMargin * 2 + kThumbnailHeight + kLabelHeight; 46 const int kListColumns = 3; 47 const int kTotalListWidth = kListColumns * kListItemWidth; 48 49 const int kDesktopMediaSourceViewGroupId = 1; 50 51 const char kDesktopMediaSourceViewClassName[] = 52 "DesktopMediaPicker_DesktopMediaSourceView"; 53 54 DesktopMediaID::Id AcceleratedWidgetToDesktopMediaId( 55 gfx::AcceleratedWidget accelerated_widget) { 56 #if defined(OS_WIN) 57 return reinterpret_cast<DesktopMediaID::Id>(accelerated_widget); 58 #else 59 return static_cast<DesktopMediaID::Id>(accelerated_widget); 60 #endif 61 } 62 63 int GetMediaListViewHeightForRows(size_t rows) { 64 return kListItemHeight * rows; 65 } 66 67 } // namespace 68 69 DesktopMediaSourceView::DesktopMediaSourceView( 70 DesktopMediaListView* parent, 71 DesktopMediaID source_id) 72 : parent_(parent), 73 source_id_(source_id), 74 image_view_(new views::ImageView()), 75 label_(new views::Label()), 76 selected_(false) { 77 AddChildView(image_view_); 78 AddChildView(label_); 79 SetFocusable(true); 80 } 81 82 DesktopMediaSourceView::~DesktopMediaSourceView() {} 83 84 void DesktopMediaSourceView::SetName(const base::string16& name) { 85 label_->SetText(name); 86 } 87 88 void DesktopMediaSourceView::SetThumbnail(const gfx::ImageSkia& thumbnail) { 89 image_view_->SetImage(thumbnail); 90 } 91 92 void DesktopMediaSourceView::SetSelected(bool selected) { 93 if (selected == selected_) 94 return; 95 selected_ = selected; 96 97 if (selected) { 98 // Unselect all other sources. 99 Views neighbours; 100 parent()->GetViewsInGroup(GetGroup(), &neighbours); 101 for (Views::iterator i(neighbours.begin()); i != neighbours.end(); ++i) { 102 if (*i != this) { 103 DCHECK_EQ((*i)->GetClassName(), kDesktopMediaSourceViewClassName); 104 DesktopMediaSourceView* source_view = 105 static_cast<DesktopMediaSourceView*>(*i); 106 source_view->SetSelected(false); 107 } 108 } 109 110 const SkColor bg_color = GetNativeTheme()->GetSystemColor( 111 ui::NativeTheme::kColorId_FocusedMenuItemBackgroundColor); 112 set_background(views::Background::CreateSolidBackground(bg_color)); 113 114 parent_->OnSelectionChanged(); 115 } else { 116 set_background(NULL); 117 } 118 119 SchedulePaint(); 120 } 121 122 const char* DesktopMediaSourceView::GetClassName() const { 123 return kDesktopMediaSourceViewClassName; 124 } 125 126 void DesktopMediaSourceView::Layout() { 127 image_view_->SetBounds(kThumbnailMargin, kThumbnailMargin, 128 kThumbnailWidth, kThumbnailHeight); 129 label_->SetBounds(kThumbnailMargin, kThumbnailHeight + kThumbnailMargin, 130 kThumbnailWidth, kLabelHeight); 131 } 132 133 views::View* DesktopMediaSourceView::GetSelectedViewForGroup(int group) { 134 Views neighbours; 135 parent()->GetViewsInGroup(group, &neighbours); 136 if (neighbours.empty()) 137 return NULL; 138 139 for (Views::iterator i(neighbours.begin()); i != neighbours.end(); ++i) { 140 DCHECK_EQ((*i)->GetClassName(), kDesktopMediaSourceViewClassName); 141 DesktopMediaSourceView* source_view = 142 static_cast<DesktopMediaSourceView*>(*i); 143 if (source_view->selected_) 144 return source_view; 145 } 146 return NULL; 147 } 148 149 bool DesktopMediaSourceView::IsGroupFocusTraversable() const { 150 return false; 151 } 152 153 void DesktopMediaSourceView::OnPaint(gfx::Canvas* canvas) { 154 View::OnPaint(canvas); 155 if (HasFocus()) { 156 gfx::Rect bounds(GetLocalBounds()); 157 bounds.Inset(kThumbnailMargin / 2, kThumbnailMargin / 2); 158 canvas->DrawFocusRect(bounds); 159 } 160 } 161 162 void DesktopMediaSourceView::OnFocus() { 163 View::OnFocus(); 164 SetSelected(true); 165 ScrollRectToVisible(gfx::Rect(size())); 166 // We paint differently when focused. 167 SchedulePaint(); 168 } 169 170 void DesktopMediaSourceView::OnBlur() { 171 View::OnBlur(); 172 // We paint differently when focused. 173 SchedulePaint(); 174 } 175 176 bool DesktopMediaSourceView::OnMousePressed(const ui::MouseEvent& event) { 177 if (event.GetClickCount() == 1) { 178 RequestFocus(); 179 } else if (event.GetClickCount() == 2) { 180 RequestFocus(); 181 parent_->OnDoubleClick(); 182 } 183 return true; 184 } 185 186 void DesktopMediaSourceView::OnGestureEvent(ui::GestureEvent* event) { 187 if (event->type() == ui::ET_GESTURE_TAP && 188 event->details().tap_count() == 2) { 189 RequestFocus(); 190 parent_->OnDoubleClick(); 191 event->SetHandled(); 192 return; 193 } 194 195 // Detect tap gesture using ET_GESTURE_TAP_DOWN so the view also gets focused 196 // on the long tap (when the tap gesture starts). 197 if (event->type() == ui::ET_GESTURE_TAP_DOWN) { 198 RequestFocus(); 199 event->SetHandled(); 200 } 201 } 202 203 DesktopMediaListView::DesktopMediaListView( 204 DesktopMediaPickerDialogView* parent, 205 scoped_ptr<DesktopMediaList> media_list) 206 : parent_(parent), 207 media_list_(media_list.Pass()), 208 weak_factory_(this) { 209 media_list_->SetThumbnailSize(gfx::Size(kThumbnailWidth, kThumbnailHeight)); 210 } 211 212 DesktopMediaListView::~DesktopMediaListView() {} 213 214 void DesktopMediaListView::StartUpdating(DesktopMediaID::Id dialog_window_id) { 215 media_list_->SetViewDialogWindowId(dialog_window_id); 216 media_list_->StartUpdating(this); 217 } 218 219 void DesktopMediaListView::OnSelectionChanged() { 220 parent_->OnSelectionChanged(); 221 } 222 223 void DesktopMediaListView::OnDoubleClick() { 224 parent_->OnDoubleClick(); 225 } 226 227 DesktopMediaSourceView* DesktopMediaListView::GetSelection() { 228 for (int i = 0; i < child_count(); ++i) { 229 DesktopMediaSourceView* source_view = 230 static_cast<DesktopMediaSourceView*>(child_at(i)); 231 DCHECK_EQ(source_view->GetClassName(), kDesktopMediaSourceViewClassName); 232 if (source_view->is_selected()) 233 return source_view; 234 } 235 return NULL; 236 } 237 238 gfx::Size DesktopMediaListView::GetPreferredSize() const { 239 int total_rows = (child_count() + kListColumns - 1) / kListColumns; 240 return gfx::Size(kTotalListWidth, GetMediaListViewHeightForRows(total_rows)); 241 } 242 243 void DesktopMediaListView::Layout() { 244 int x = 0; 245 int y = 0; 246 247 for (int i = 0; i < child_count(); ++i) { 248 if (x + kListItemWidth > kTotalListWidth) { 249 x = 0; 250 y += kListItemHeight; 251 } 252 253 View* source_view = child_at(i); 254 source_view->SetBounds(x, y, kListItemWidth, kListItemHeight); 255 256 x += kListItemWidth; 257 } 258 259 y += kListItemHeight; 260 SetSize(gfx::Size(kTotalListWidth, y)); 261 } 262 263 bool DesktopMediaListView::OnKeyPressed(const ui::KeyEvent& event) { 264 int position_increment = 0; 265 switch (event.key_code()) { 266 case ui::VKEY_UP: 267 position_increment = -kListColumns; 268 break; 269 case ui::VKEY_DOWN: 270 position_increment = kListColumns; 271 break; 272 case ui::VKEY_LEFT: 273 position_increment = -1; 274 break; 275 case ui::VKEY_RIGHT: 276 position_increment = 1; 277 break; 278 default: 279 return false; 280 } 281 282 if (position_increment != 0) { 283 DesktopMediaSourceView* selected = GetSelection(); 284 DesktopMediaSourceView* new_selected = NULL; 285 286 if (selected) { 287 int index = GetIndexOf(selected); 288 int new_index = index + position_increment; 289 if (new_index >= child_count()) 290 new_index = child_count() - 1; 291 else if (new_index < 0) 292 new_index = 0; 293 if (index != new_index) { 294 new_selected = 295 static_cast<DesktopMediaSourceView*>(child_at(new_index)); 296 } 297 } else if (has_children()) { 298 new_selected = static_cast<DesktopMediaSourceView*>(child_at(0)); 299 } 300 301 if (new_selected) { 302 GetFocusManager()->SetFocusedView(new_selected); 303 } 304 305 return true; 306 } 307 308 return false; 309 } 310 311 void DesktopMediaListView::OnSourceAdded(int index) { 312 const DesktopMediaList::Source& source = media_list_->GetSource(index); 313 DesktopMediaSourceView* source_view = 314 new DesktopMediaSourceView(this, source.id); 315 source_view->SetName(source.name); 316 source_view->SetGroup(kDesktopMediaSourceViewGroupId); 317 AddChildViewAt(source_view, index); 318 319 PreferredSizeChanged(); 320 321 if (child_count() % kListColumns == 1) 322 parent_->OnMediaListRowsChanged(); 323 324 std::string autoselect_source = 325 CommandLine::ForCurrentProcess()->GetSwitchValueASCII( 326 switches::kAutoSelectDesktopCaptureSource); 327 if (!autoselect_source.empty() && 328 base::ASCIIToUTF16(autoselect_source) == source.name) { 329 // Select, then accept and close the dialog when we're done adding sources. 330 source_view->OnFocus(); 331 content::BrowserThread::PostTask( 332 content::BrowserThread::UI, FROM_HERE, 333 base::Bind(&DesktopMediaListView::AcceptSelection, 334 weak_factory_.GetWeakPtr())); 335 } 336 } 337 338 void DesktopMediaListView::OnSourceRemoved(int index) { 339 DesktopMediaSourceView* view = 340 static_cast<DesktopMediaSourceView*>(child_at(index)); 341 DCHECK(view); 342 DCHECK_EQ(view->GetClassName(), kDesktopMediaSourceViewClassName); 343 bool was_selected = view->is_selected(); 344 RemoveChildView(view); 345 delete view; 346 347 if (was_selected) 348 OnSelectionChanged(); 349 350 PreferredSizeChanged(); 351 352 if (child_count() % kListColumns == 0) 353 parent_->OnMediaListRowsChanged(); 354 } 355 356 void DesktopMediaListView::OnSourceMoved(int old_index, int new_index) { 357 DesktopMediaSourceView* view = 358 static_cast<DesktopMediaSourceView*>(child_at(old_index)); 359 ReorderChildView(view, new_index); 360 PreferredSizeChanged(); 361 } 362 363 void DesktopMediaListView::OnSourceNameChanged(int index) { 364 const DesktopMediaList::Source& source = media_list_->GetSource(index); 365 DesktopMediaSourceView* source_view = 366 static_cast<DesktopMediaSourceView*>(child_at(index)); 367 source_view->SetName(source.name); 368 } 369 370 void DesktopMediaListView::OnSourceThumbnailChanged(int index) { 371 const DesktopMediaList::Source& source = media_list_->GetSource(index); 372 DesktopMediaSourceView* source_view = 373 static_cast<DesktopMediaSourceView*>(child_at(index)); 374 source_view->SetThumbnail(source.thumbnail); 375 } 376 377 void DesktopMediaListView::AcceptSelection() { 378 OnSelectionChanged(); 379 OnDoubleClick(); 380 } 381 382 DesktopMediaPickerDialogView::DesktopMediaPickerDialogView( 383 content::WebContents* parent_web_contents, 384 gfx::NativeWindow context, 385 DesktopMediaPickerViews* parent, 386 const base::string16& app_name, 387 const base::string16& target_name, 388 scoped_ptr<DesktopMediaList> media_list) 389 : parent_(parent), 390 app_name_(app_name), 391 label_(new views::Label()), 392 scroll_view_(views::ScrollView::CreateScrollViewWithBorder()), 393 list_view_(new DesktopMediaListView(this, media_list.Pass())) { 394 if (app_name == target_name) { 395 label_->SetText( 396 l10n_util::GetStringFUTF16(IDS_DESKTOP_MEDIA_PICKER_TEXT, app_name)); 397 } else { 398 label_->SetText(l10n_util::GetStringFUTF16( 399 IDS_DESKTOP_MEDIA_PICKER_TEXT_DELEGATED, app_name, target_name)); 400 } 401 label_->SetMultiLine(true); 402 label_->SetHorizontalAlignment(gfx::ALIGN_LEFT); 403 AddChildView(label_); 404 405 scroll_view_->SetContents(list_view_); 406 scroll_view_->ClipHeightTo( 407 GetMediaListViewHeightForRows(1), GetMediaListViewHeightForRows(2)); 408 AddChildView(scroll_view_); 409 410 // If |parent_web_contents| is set and it's not a background page then the 411 // picker will be shown modal to the web contents. Otherwise the picker is 412 // shown in a separate window. 413 views::Widget* widget = NULL; 414 bool modal_dialog = 415 parent_web_contents && 416 !parent_web_contents->GetDelegate()->IsNeverVisible(parent_web_contents); 417 if (modal_dialog) { 418 widget = CreateWebModalDialogViews(this, parent_web_contents); 419 } else { 420 widget = DialogDelegate::CreateDialogWidget(this, context, NULL); 421 } 422 423 // If the picker is not modal to the calling web contents then it is displayed 424 // in its own top-level window, so in that case it needs to be filtered out of 425 // the list of top-level windows available for capture, and to achieve that 426 // the Id is passed to DesktopMediaList. 427 DesktopMediaID::Id dialog_window_id = 0; 428 if (!modal_dialog) { 429 #if defined(USE_ASH) 430 if (chrome::IsNativeWindowInAsh(widget->GetNativeWindow())) { 431 dialog_window_id = 432 DesktopMediaID::RegisterAuraWindow(widget->GetNativeWindow()).id; 433 DCHECK_NE(dialog_window_id, 0); 434 } 435 #endif 436 437 if (dialog_window_id == 0) { 438 dialog_window_id = AcceleratedWidgetToDesktopMediaId( 439 widget->GetNativeWindow()->GetHost()->GetAcceleratedWidget()); 440 } 441 } 442 443 list_view_->StartUpdating(dialog_window_id); 444 445 if (modal_dialog) { 446 web_modal::PopupManager* popup_manager = 447 web_modal::PopupManager::FromWebContents(parent_web_contents); 448 popup_manager->ShowModalDialog(GetWidget()->GetNativeView(), 449 parent_web_contents); 450 } else { 451 widget->Show(); 452 } 453 } 454 455 DesktopMediaPickerDialogView::~DesktopMediaPickerDialogView() {} 456 457 void DesktopMediaPickerDialogView::DetachParent() { 458 parent_ = NULL; 459 } 460 461 gfx::Size DesktopMediaPickerDialogView::GetPreferredSize() const { 462 static const size_t kDialogViewWidth = 600; 463 const gfx::Insets title_insets = views::BubbleFrameView::GetTitleInsets(); 464 size_t label_height = 465 label_->GetHeightForWidth(kDialogViewWidth - title_insets.height() * 2); 466 467 return gfx::Size(kDialogViewWidth, 468 views::kPanelVertMargin * 2 + label_height + 469 views::kPanelVerticalSpacing + 470 scroll_view_->GetPreferredSize().height()); 471 } 472 473 void DesktopMediaPickerDialogView::Layout() { 474 // DialogDelegate uses the bubble style frame. 475 const gfx::Insets title_insets = views::BubbleFrameView::GetTitleInsets(); 476 gfx::Rect rect = GetLocalBounds(); 477 478 rect.Inset(title_insets.left(), views::kPanelVertMargin); 479 480 gfx::Rect label_rect(rect.x(), rect.y(), rect.width(), 481 label_->GetHeightForWidth(rect.width())); 482 label_->SetBoundsRect(label_rect); 483 484 int scroll_view_top = label_rect.bottom() + views::kPanelVerticalSpacing; 485 scroll_view_->SetBounds( 486 rect.x(), scroll_view_top, 487 rect.width(), rect.height() - scroll_view_top); 488 } 489 490 ui::ModalType DesktopMediaPickerDialogView::GetModalType() const { 491 return ui::MODAL_TYPE_CHILD; 492 } 493 494 base::string16 DesktopMediaPickerDialogView::GetWindowTitle() const { 495 return l10n_util::GetStringFUTF16(IDS_DESKTOP_MEDIA_PICKER_TITLE, app_name_); 496 } 497 498 bool DesktopMediaPickerDialogView::IsDialogButtonEnabled( 499 ui::DialogButton button) const { 500 if (button == ui::DIALOG_BUTTON_OK) 501 return list_view_->GetSelection() != NULL; 502 return true; 503 } 504 505 base::string16 DesktopMediaPickerDialogView::GetDialogButtonLabel( 506 ui::DialogButton button) const { 507 return l10n_util::GetStringUTF16(button == ui::DIALOG_BUTTON_OK ? 508 IDS_DESKTOP_MEDIA_PICKER_SHARE : IDS_CANCEL); 509 } 510 511 bool DesktopMediaPickerDialogView::Accept() { 512 DesktopMediaSourceView* selection = list_view_->GetSelection(); 513 514 // Ok button should only be enabled when a source is selected. 515 DCHECK(selection); 516 517 DesktopMediaID source; 518 if (selection) 519 source = selection->source_id(); 520 521 if (parent_) 522 parent_->NotifyDialogResult(source); 523 524 // Return true to close the window. 525 return true; 526 } 527 528 void DesktopMediaPickerDialogView::DeleteDelegate() { 529 // If the dialog is being closed then notify the parent about it. 530 if (parent_) 531 parent_->NotifyDialogResult(DesktopMediaID()); 532 delete this; 533 } 534 535 void DesktopMediaPickerDialogView::OnSelectionChanged() { 536 GetDialogClientView()->UpdateDialogButtons(); 537 } 538 539 void DesktopMediaPickerDialogView::OnDoubleClick() { 540 // This will call Accept() and close the dialog. 541 GetDialogClientView()->AcceptWindow(); 542 } 543 544 void DesktopMediaPickerDialogView::OnMediaListRowsChanged() { 545 gfx::Rect widget_bound = GetWidget()->GetWindowBoundsInScreen(); 546 547 int new_height = widget_bound.height() - scroll_view_->height() + 548 scroll_view_->GetPreferredSize().height(); 549 550 GetWidget()->CenterWindow(gfx::Size(widget_bound.width(), new_height)); 551 } 552 553 DesktopMediaSourceView* 554 DesktopMediaPickerDialogView::GetMediaSourceViewForTesting(int index) const { 555 if (list_view_->child_count() <= index) 556 return NULL; 557 558 return reinterpret_cast<DesktopMediaSourceView*>(list_view_->child_at(index)); 559 } 560 561 DesktopMediaPickerViews::DesktopMediaPickerViews() : dialog_(NULL) { 562 } 563 564 DesktopMediaPickerViews::~DesktopMediaPickerViews() { 565 if (dialog_) { 566 dialog_->DetachParent(); 567 dialog_->GetWidget()->Close(); 568 } 569 } 570 571 void DesktopMediaPickerViews::Show(content::WebContents* web_contents, 572 gfx::NativeWindow context, 573 gfx::NativeWindow parent, 574 const base::string16& app_name, 575 const base::string16& target_name, 576 scoped_ptr<DesktopMediaList> media_list, 577 const DoneCallback& done_callback) { 578 callback_ = done_callback; 579 dialog_ = new DesktopMediaPickerDialogView( 580 web_contents, context, this, app_name, target_name, media_list.Pass()); 581 } 582 583 void DesktopMediaPickerViews::NotifyDialogResult(DesktopMediaID source) { 584 // Once this method is called the |dialog_| will close and destroy itself. 585 dialog_->DetachParent(); 586 dialog_ = NULL; 587 588 DCHECK(!callback_.is_null()); 589 590 // Notify the |callback_| asynchronously because it may need to destroy 591 // DesktopMediaPicker. 592 content::BrowserThread::PostTask( 593 content::BrowserThread::UI, FROM_HERE, 594 base::Bind(callback_, source)); 595 callback_.Reset(); 596 } 597 598 // static 599 scoped_ptr<DesktopMediaPicker> DesktopMediaPicker::Create() { 600 return scoped_ptr<DesktopMediaPicker>(new DesktopMediaPickerViews()); 601 } 602