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