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