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