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/media/desktop_media_picker.h" 6 7#include "ash/shell.h" 8#include "base/callback.h" 9#include "chrome/browser/media/desktop_media_list.h" 10#include "chrome/browser/media/desktop_media_list_observer.h" 11#include "content/public/browser/browser_thread.h" 12#include "grit/generated_resources.h" 13#include "ui/base/l10n/l10n_util.h" 14#include "ui/events/keycodes/keyboard_codes.h" 15#include "ui/gfx/canvas.h" 16#include "ui/native_theme/native_theme.h" 17#include "ui/views/background.h" 18#include "ui/views/controls/image_view.h" 19#include "ui/views/controls/label.h" 20#include "ui/views/controls/scroll_view.h" 21#include "ui/views/corewm/shadow_types.h" 22#include "ui/views/layout/box_layout.h" 23#include "ui/views/layout/layout_constants.h" 24#include "ui/views/widget/widget.h" 25#include "ui/views/window/dialog_client_view.h" 26#include "ui/views/window/dialog_delegate.h" 27 28#if defined(USE_AURA) 29#include "ui/aura/root_window.h" 30#endif 31 32#if defined(OS_WIN) 33#include "ui/views/win/hwnd_util.h" 34#endif 35 36using content::DesktopMediaID; 37 38namespace { 39 40const int kThumbnailWidth = 160; 41const int kThumbnailHeight = 100; 42const int kThumbnailMargin = 10; 43const int kLabelHeight = 40; 44const int kListItemWidth = kThumbnailMargin * 2 + kThumbnailWidth; 45const int kListItemHeight = 46 kThumbnailMargin * 2 + kThumbnailHeight + kLabelHeight; 47const int kListColumns = 3; 48const int kTotalListWidth = kListColumns * kListItemWidth; 49 50const int kDesktopMediaSourceViewGroupId = 1; 51 52const char kDesktopMediaSourceViewClassName[] = 53 "DesktopMediaPicker_DesktopMediaSourceView"; 54 55class DesktopMediaListView; 56class DesktopMediaPickerDialogView; 57class DesktopMediaPickerViews; 58 59// View used for each item in DesktopMediaListView. Shows a single desktop media 60// source as a thumbnail with the title under it. 61class DesktopMediaSourceView : public views::View { 62 public: 63 DesktopMediaSourceView(DesktopMediaListView* parent, 64 DesktopMediaID source_id); 65 virtual ~DesktopMediaSourceView(); 66 67 // Updates thumbnail and title from |source|. 68 void SetName(const base::string16& name); 69 void SetThumbnail(const gfx::ImageSkia& thumbnail); 70 71 // Id for the source shown by this View. 72 const DesktopMediaID& source_id() const { 73 return source_id_; 74 } 75 76 // Returns true if the source is selected. 77 bool is_selected() const { return selected_; } 78 79 // Updates selection state of the element. If |selected| is true then also 80 // calls SetSelected(false) for the source view that was selected before that 81 // (if any). 82 void SetSelected(bool selected); 83 84 // views::View interface. 85 virtual const char* GetClassName() const OVERRIDE; 86 virtual void Layout() OVERRIDE; 87 virtual views::View* GetSelectedViewForGroup(int group) OVERRIDE; 88 virtual bool IsGroupFocusTraversable() const OVERRIDE; 89 virtual void OnPaint(gfx::Canvas* canvas) OVERRIDE; 90 virtual void OnFocus() OVERRIDE; 91 virtual void OnBlur() OVERRIDE; 92 virtual bool OnMousePressed(const ui::MouseEvent& event) OVERRIDE; 93 94 private: 95 DesktopMediaListView* parent_; 96 DesktopMediaID source_id_; 97 98 views::ImageView* image_view_; 99 views::Label* label_; 100 101 bool selected_; 102 103 DISALLOW_COPY_AND_ASSIGN(DesktopMediaSourceView); 104}; 105 106// View that shows list of desktop media sources available from 107// DesktopMediaList. 108class DesktopMediaListView : public views::View, 109 public DesktopMediaListObserver { 110 public: 111 DesktopMediaListView(DesktopMediaPickerDialogView* parent, 112 scoped_ptr<DesktopMediaList> media_list); 113 virtual ~DesktopMediaListView(); 114 115 void StartUpdating(content::DesktopMediaID::Id dialog_window_id); 116 117 // Called by DesktopMediaSourceView when selection has changed. 118 void OnSelectionChanged(); 119 120 // Called by DesktopMediaSourceView when a source has been double-clicked. 121 void OnDoubleClick(); 122 123 // Returns currently selected source. 124 DesktopMediaSourceView* GetSelection(); 125 126 // views::View overrides. 127 virtual gfx::Size GetPreferredSize() OVERRIDE; 128 virtual void Layout() OVERRIDE; 129 virtual bool OnKeyPressed(const ui::KeyEvent& event) OVERRIDE; 130 131 private: 132 // DesktopMediaList::Observer interface 133 virtual void OnSourceAdded(int index) OVERRIDE; 134 virtual void OnSourceRemoved(int index) OVERRIDE; 135 virtual void OnSourceNameChanged(int index) OVERRIDE; 136 virtual void OnSourceThumbnailChanged(int index) OVERRIDE; 137 138 DesktopMediaPickerDialogView* parent_; 139 scoped_ptr<DesktopMediaList> media_list_; 140 141 DISALLOW_COPY_AND_ASSIGN(DesktopMediaListView); 142}; 143 144// Dialog view used for DesktopMediaPickerViews. 145class DesktopMediaPickerDialogView : public views::DialogDelegateView { 146 public: 147 DesktopMediaPickerDialogView(gfx::NativeWindow context, 148 gfx::NativeWindow parent_window, 149 DesktopMediaPickerViews* parent, 150 const base::string16& app_name, 151 scoped_ptr<DesktopMediaList> media_list); 152 virtual ~DesktopMediaPickerDialogView(); 153 154 // Called by parent (DesktopMediaPickerViews) when it's destroyed. 155 void DetachParent(); 156 157 // Called by DesktopMediaListView. 158 void OnSelectionChanged(); 159 void OnDoubleClick(); 160 161 // views::View overrides. 162 virtual gfx::Size GetPreferredSize() OVERRIDE; 163 virtual void Layout() OVERRIDE; 164 165 // views::DialogDelegateView overrides. 166 virtual base::string16 GetWindowTitle() const OVERRIDE; 167 virtual bool IsDialogButtonEnabled(ui::DialogButton button) const OVERRIDE; 168 virtual bool Accept() OVERRIDE; 169 virtual void DeleteDelegate() OVERRIDE; 170 171 private: 172 DesktopMediaPickerViews* parent_; 173 base::string16 app_name_; 174 175 views::Label* label_; 176 views::ScrollView* scroll_view_; 177 DesktopMediaListView* list_view_; 178 179 DISALLOW_COPY_AND_ASSIGN(DesktopMediaPickerDialogView); 180}; 181 182// Implementation of DesktopMediaPicker for Views. 183class DesktopMediaPickerViews : public DesktopMediaPicker { 184 public: 185 DesktopMediaPickerViews(); 186 virtual ~DesktopMediaPickerViews(); 187 188 void NotifyDialogResult(DesktopMediaID source); 189 190 // DesktopMediaPicker overrides. 191 virtual void Show(gfx::NativeWindow context, 192 gfx::NativeWindow parent, 193 const base::string16& app_name, 194 scoped_ptr<DesktopMediaList> media_list, 195 const DoneCallback& done_callback) OVERRIDE; 196 197 private: 198 DoneCallback callback_; 199 200 // The |dialog_| is owned by the corresponding views::Widget instance. 201 // When DesktopMediaPickerViews is destroyed the |dialog_| is destroyed 202 // asynchronously by closing the widget. 203 DesktopMediaPickerDialogView* dialog_; 204 205 DISALLOW_COPY_AND_ASSIGN(DesktopMediaPickerViews); 206}; 207 208DesktopMediaSourceView::DesktopMediaSourceView( 209 DesktopMediaListView* parent, 210 DesktopMediaID source_id) 211 : parent_(parent), 212 source_id_(source_id), 213 image_view_(new views::ImageView()), 214 label_(new views::Label()), 215 selected_(false) { 216 AddChildView(image_view_); 217 AddChildView(label_); 218 SetFocusable(true); 219} 220 221DesktopMediaSourceView::~DesktopMediaSourceView() {} 222 223void DesktopMediaSourceView::SetName(const base::string16& name) { 224 label_->SetText(name); 225} 226 227void DesktopMediaSourceView::SetThumbnail(const gfx::ImageSkia& thumbnail) { 228 image_view_->SetImage(thumbnail); 229} 230 231void DesktopMediaSourceView::SetSelected(bool selected) { 232 if (selected == selected_) 233 return; 234 selected_ = selected; 235 236 if (selected) { 237 // Unselect all other sources. 238 Views neighbours; 239 parent()->GetViewsInGroup(GetGroup(), &neighbours); 240 for (Views::iterator i(neighbours.begin()); i != neighbours.end(); ++i) { 241 if (*i != this) { 242 DCHECK_EQ((*i)->GetClassName(), kDesktopMediaSourceViewClassName); 243 DesktopMediaSourceView* source_view = 244 static_cast<DesktopMediaSourceView*>(*i); 245 source_view->SetSelected(false); 246 } 247 } 248 249 const SkColor bg_color = GetNativeTheme()->GetSystemColor( 250 ui::NativeTheme::kColorId_FocusedMenuItemBackgroundColor); 251 set_background(views::Background::CreateSolidBackground(bg_color)); 252 253 parent_->OnSelectionChanged(); 254 } else { 255 set_background(NULL); 256 } 257 258 SchedulePaint(); 259} 260 261const char* DesktopMediaSourceView::GetClassName() const { 262 return kDesktopMediaSourceViewClassName; 263} 264 265void DesktopMediaSourceView::Layout() { 266 image_view_->SetBounds(kThumbnailMargin, kThumbnailMargin, 267 kThumbnailWidth, kThumbnailHeight); 268 label_->SetBounds(kThumbnailMargin, kThumbnailHeight + kThumbnailMargin, 269 kThumbnailWidth, kLabelHeight); 270} 271 272views::View* DesktopMediaSourceView::GetSelectedViewForGroup(int group) { 273 Views neighbours; 274 parent()->GetViewsInGroup(group, &neighbours); 275 if (neighbours.empty()) 276 return NULL; 277 278 for (Views::iterator i(neighbours.begin()); i != neighbours.end(); ++i) { 279 DCHECK_EQ((*i)->GetClassName(), kDesktopMediaSourceViewClassName); 280 DesktopMediaSourceView* source_view = 281 static_cast<DesktopMediaSourceView*>(*i); 282 if (source_view->selected_) 283 return source_view; 284 } 285 return NULL; 286} 287 288bool DesktopMediaSourceView::IsGroupFocusTraversable() const { 289 return false; 290} 291 292void DesktopMediaSourceView::OnPaint(gfx::Canvas* canvas) { 293 View::OnPaint(canvas); 294 if (HasFocus()) { 295 gfx::Rect bounds(GetLocalBounds()); 296 bounds.Inset(kThumbnailMargin / 2, kThumbnailMargin / 2); 297 canvas->DrawFocusRect(bounds); 298 } 299} 300 301void DesktopMediaSourceView::OnFocus() { 302 View::OnFocus(); 303 SetSelected(true); 304 ScrollRectToVisible(gfx::Rect(size())); 305 // We paint differently when focused. 306 SchedulePaint(); 307} 308 309void DesktopMediaSourceView::OnBlur() { 310 View::OnBlur(); 311 // We paint differently when focused. 312 SchedulePaint(); 313} 314 315bool DesktopMediaSourceView::OnMousePressed(const ui::MouseEvent& event) { 316 if (event.GetClickCount() == 1) { 317 RequestFocus(); 318 } else if (event.GetClickCount() == 2) { 319 RequestFocus(); 320 parent_->OnDoubleClick(); 321 } 322 return true; 323} 324 325DesktopMediaListView::DesktopMediaListView( 326 DesktopMediaPickerDialogView* parent, 327 scoped_ptr<DesktopMediaList> media_list) 328 : parent_(parent), 329 media_list_(media_list.Pass()) { 330 media_list_->SetThumbnailSize(gfx::Size(kThumbnailWidth, kThumbnailHeight)); 331} 332 333DesktopMediaListView::~DesktopMediaListView() {} 334 335void DesktopMediaListView::StartUpdating( 336 content::DesktopMediaID::Id dialog_window_id) { 337 media_list_->SetViewDialogWindowId(dialog_window_id); 338 media_list_->StartUpdating(this); 339} 340 341void DesktopMediaListView::OnSelectionChanged() { 342 parent_->OnSelectionChanged(); 343} 344 345void DesktopMediaListView::OnDoubleClick() { 346 parent_->OnDoubleClick(); 347} 348 349DesktopMediaSourceView* DesktopMediaListView::GetSelection() { 350 for (int i = 0; i < child_count(); ++i) { 351 DesktopMediaSourceView* source_view = 352 static_cast<DesktopMediaSourceView*>(child_at(i)); 353 DCHECK_EQ(source_view->GetClassName(), kDesktopMediaSourceViewClassName); 354 if (source_view->is_selected()) 355 return source_view; 356 } 357 return NULL; 358} 359 360gfx::Size DesktopMediaListView::GetPreferredSize() { 361 int total_rows = (child_count() + kListColumns - 1) / kListColumns; 362 return gfx::Size(kTotalListWidth, kListItemHeight * total_rows); 363} 364 365void DesktopMediaListView::Layout() { 366 int x = 0; 367 int y = 0; 368 369 for (int i = 0; i < child_count(); ++i) { 370 if (x + kListItemWidth > kTotalListWidth) { 371 x = 0; 372 y += kListItemHeight; 373 } 374 375 View* source_view = child_at(i); 376 source_view->SetBounds(x, y, kListItemWidth, kListItemHeight); 377 378 x += kListItemWidth; 379 } 380 381 y += kListItemHeight; 382 SetSize(gfx::Size(kTotalListWidth, y)); 383} 384 385bool DesktopMediaListView::OnKeyPressed(const ui::KeyEvent& event) { 386 int position_increment = 0; 387 switch (event.key_code()) { 388 case ui::VKEY_UP: 389 position_increment = -kListColumns; 390 break; 391 case ui::VKEY_DOWN: 392 position_increment = kListColumns; 393 break; 394 case ui::VKEY_LEFT: 395 position_increment = -1; 396 break; 397 case ui::VKEY_RIGHT: 398 position_increment = 1; 399 break; 400 default: 401 return false; 402 } 403 404 if (position_increment != 0) { 405 DesktopMediaSourceView* selected = GetSelection(); 406 DesktopMediaSourceView* new_selected = NULL; 407 408 if (selected) { 409 int index = GetIndexOf(selected); 410 int new_index = index + position_increment; 411 if (new_index >= child_count()) 412 new_index = child_count() - 1; 413 else if (new_index < 0) 414 new_index = 0; 415 if (index != new_index) { 416 new_selected = 417 static_cast<DesktopMediaSourceView*>(child_at(new_index)); 418 } 419 } else if (has_children()) { 420 new_selected = static_cast<DesktopMediaSourceView*>(child_at(0)); 421 } 422 423 if (new_selected) { 424 GetFocusManager()->SetFocusedView(new_selected); 425 } 426 427 return true; 428 } 429 430 return false; 431} 432 433void DesktopMediaListView::OnSourceAdded(int index) { 434 const DesktopMediaList::Source& source = media_list_->GetSource(index); 435 DesktopMediaSourceView* source_view = 436 new DesktopMediaSourceView(this, source.id); 437 source_view->SetName(source.name); 438 source_view->SetGroup(kDesktopMediaSourceViewGroupId); 439 AddChildViewAt(source_view, index); 440 441 PreferredSizeChanged(); 442} 443 444void DesktopMediaListView::OnSourceRemoved(int index) { 445 DesktopMediaSourceView* view = 446 static_cast<DesktopMediaSourceView*>(child_at(index)); 447 DCHECK(view); 448 DCHECK_EQ(view->GetClassName(), kDesktopMediaSourceViewClassName); 449 bool was_selected = view->is_selected(); 450 RemoveChildView(view); 451 delete view; 452 453 if (was_selected) 454 OnSelectionChanged(); 455 456 PreferredSizeChanged(); 457} 458 459void DesktopMediaListView::OnSourceNameChanged(int index) { 460 const DesktopMediaList::Source& source = media_list_->GetSource(index); 461 DesktopMediaSourceView* source_view = 462 static_cast<DesktopMediaSourceView*>(child_at(index)); 463 source_view->SetName(source.name); 464} 465 466void DesktopMediaListView::OnSourceThumbnailChanged(int index) { 467 const DesktopMediaList::Source& source = media_list_->GetSource(index); 468 DesktopMediaSourceView* source_view = 469 static_cast<DesktopMediaSourceView*>(child_at(index)); 470 source_view->SetThumbnail(source.thumbnail); 471} 472 473DesktopMediaPickerDialogView::DesktopMediaPickerDialogView( 474 gfx::NativeWindow context, 475 gfx::NativeWindow parent_window, 476 DesktopMediaPickerViews* parent, 477 const base::string16& app_name, 478 scoped_ptr<DesktopMediaList> media_list) 479 : parent_(parent), 480 app_name_(app_name), 481 label_(new views::Label()), 482 scroll_view_(views::ScrollView::CreateScrollViewWithBorder()), 483 list_view_(new DesktopMediaListView(this, media_list.Pass())) { 484 label_->SetText( 485 l10n_util::GetStringFUTF16(IDS_DESKTOP_MEDIA_PICKER_TEXT, app_name_)); 486 label_->SetMultiLine(true); 487 label_->SetHorizontalAlignment(gfx::ALIGN_LEFT); 488 AddChildView(label_); 489 490 scroll_view_->SetContents(list_view_); 491 AddChildView(scroll_view_); 492 493 DialogDelegate::CreateDialogWidget(this, context, parent_window); 494 495 // DesktopMediaList needs to know the ID of the picker window which 496 // matches the ID it gets from the OS. Depending on the OS and configuration 497 // we get this ID differently. 498 // 499 // TODO(sergeyu): Update this code when Ash-specific window capturer is 500 // implemented. Currently this code will always get native windows ID 501 // which is not what we need in Ash. http://crbug.com/295102 502 content::DesktopMediaID::Id dialog_window_id; 503 504#if defined(OS_WIN) 505 dialog_window_id = reinterpret_cast<content::DesktopMediaID::Id>( 506 views::HWNDForWidget(GetWidget())); 507#elif defined(USE_AURA) 508 dialog_window_id = static_cast<content::DesktopMediaID::Id>( 509 GetWidget()->GetNativeWindow()->GetDispatcher()->host()-> 510 GetAcceleratedWidget()); 511#else 512 dialog_window_id = 0; 513 NOTIMPLEMENTED(); 514#endif 515 516 list_view_->StartUpdating(dialog_window_id); 517 518 GetWidget()->Show(); 519} 520 521DesktopMediaPickerDialogView::~DesktopMediaPickerDialogView() {} 522 523void DesktopMediaPickerDialogView::DetachParent() { 524 parent_ = NULL; 525} 526 527gfx::Size DesktopMediaPickerDialogView::GetPreferredSize() { 528 return gfx::Size(600, 500); 529} 530 531void DesktopMediaPickerDialogView::Layout() { 532 gfx::Rect rect = GetLocalBounds(); 533 rect.Inset(views::kPanelHorizMargin, views::kPanelVertMargin); 534 535 gfx::Rect label_rect(rect.x(), rect.y(), rect.width(), 536 label_->GetHeightForWidth(rect.width())); 537 label_->SetBoundsRect(label_rect); 538 539 int scroll_view_top = label_rect.bottom() + views::kPanelVerticalSpacing; 540 scroll_view_->SetBounds( 541 rect.x(), scroll_view_top, 542 rect.width(), rect.height() - scroll_view_top); 543} 544 545base::string16 DesktopMediaPickerDialogView::GetWindowTitle() const { 546 return l10n_util::GetStringFUTF16(IDS_DESKTOP_MEDIA_PICKER_TITLE, app_name_); 547} 548 549bool DesktopMediaPickerDialogView::IsDialogButtonEnabled( 550 ui::DialogButton button) const { 551 if (button == ui::DIALOG_BUTTON_OK) 552 return list_view_->GetSelection() != NULL; 553 return true; 554} 555 556bool DesktopMediaPickerDialogView::Accept() { 557 DesktopMediaSourceView* selection = list_view_->GetSelection(); 558 559 // Ok button should only be enabled when a source is selected. 560 DCHECK(selection); 561 562 DesktopMediaID source; 563 if (selection) 564 source = selection->source_id(); 565 566 if (parent_) 567 parent_->NotifyDialogResult(source); 568 569 // Return true to close the window. 570 return true; 571} 572 573void DesktopMediaPickerDialogView::DeleteDelegate() { 574 // If the dialog is being closed then notify the parent about it. 575 if (parent_) 576 parent_->NotifyDialogResult(DesktopMediaID()); 577 delete this; 578} 579 580void DesktopMediaPickerDialogView::OnSelectionChanged() { 581 GetDialogClientView()->UpdateDialogButtons(); 582} 583 584void DesktopMediaPickerDialogView::OnDoubleClick() { 585 // This will call Accept() and close the dialog. 586 GetDialogClientView()->AcceptWindow(); 587} 588 589DesktopMediaPickerViews::DesktopMediaPickerViews() 590 : dialog_(NULL) { 591} 592 593DesktopMediaPickerViews::~DesktopMediaPickerViews() { 594 if (dialog_) { 595 dialog_->DetachParent(); 596 dialog_->GetWidget()->Close(); 597 } 598} 599 600void DesktopMediaPickerViews::Show(gfx::NativeWindow context, 601 gfx::NativeWindow parent, 602 const base::string16& app_name, 603 scoped_ptr<DesktopMediaList> media_list, 604 const DoneCallback& done_callback) { 605 callback_ = done_callback; 606 dialog_ = new DesktopMediaPickerDialogView( 607 context, parent, this, app_name, media_list.Pass()); 608} 609 610void DesktopMediaPickerViews::NotifyDialogResult( 611 DesktopMediaID source) { 612 // Once this method is called the |dialog_| will close and destroy itself. 613 dialog_->DetachParent(); 614 dialog_ = NULL; 615 616 DCHECK(!callback_.is_null()); 617 618 // Notify the |callback_| asynchronously because it may need to destroy 619 // DesktopMediaPicker. 620 content::BrowserThread::PostTask( 621 content::BrowserThread::UI, FROM_HERE, 622 base::Bind(callback_, source)); 623 callback_.Reset(); 624} 625 626} // namespace 627 628// static 629scoped_ptr<DesktopMediaPicker> DesktopMediaPicker::Create() { 630 return scoped_ptr<DesktopMediaPicker>(new DesktopMediaPickerViews()); 631} 632