desktop_media_picker_views.cc revision a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7
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  set_focusable(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