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