desktop_media_picker_views.cc revision 68043e1e95eeb07d5cae7aca370b26518b0867d6
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/l10n/l10n_util.h"
13#include "ui/events/keycodes/keyboard_codes.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#if defined(USE_AURA)
27#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  // Returns currently selected source.
117  DesktopMediaSourceView* GetSelection();
118
119  // views::View overrides.
120  virtual gfx::Size GetPreferredSize() OVERRIDE;
121  virtual void Layout() OVERRIDE;
122  virtual bool OnKeyPressed(const ui::KeyEvent& event) OVERRIDE;
123
124 private:
125  // DesktopMediaPickerModel::Observer interface
126  virtual void OnSourceAdded(int index) OVERRIDE;
127  virtual void OnSourceRemoved(int index) OVERRIDE;
128  virtual void OnSourceNameChanged(int index) OVERRIDE;
129  virtual void OnSourceThumbnailChanged(int index) OVERRIDE;
130
131  DesktopMediaPickerDialogView* parent_;
132  scoped_ptr<DesktopMediaPickerModel> model_;
133
134  DISALLOW_COPY_AND_ASSIGN(DesktopMediaListView);
135};
136
137// Dialog view used for DesktopMediaPickerViews.
138class DesktopMediaPickerDialogView : public views::DialogDelegateView {
139 public:
140  DesktopMediaPickerDialogView(gfx::NativeWindow context,
141                               gfx::NativeWindow parent_window,
142                               DesktopMediaPickerViews* parent,
143                               const string16& app_name,
144                               scoped_ptr<DesktopMediaPickerModel> model);
145  virtual ~DesktopMediaPickerDialogView();
146
147  // Called by parent (DesktopMediaPickerViews) when it's destroyed.
148  void DetachParent();
149
150  // Called by DesktopMediaListView.
151  void OnSelectionChanged();
152
153  // views::View overrides.
154  virtual gfx::Size GetPreferredSize() OVERRIDE;
155  virtual void Layout() OVERRIDE;
156
157  // views::DialogDelegateView overrides.
158  virtual base::string16 GetWindowTitle() const OVERRIDE;
159  virtual bool IsDialogButtonEnabled(ui::DialogButton button) const OVERRIDE;
160  virtual bool Accept() OVERRIDE;
161  virtual void DeleteDelegate() OVERRIDE;
162
163 private:
164  DesktopMediaPickerViews* parent_;
165  string16 app_name_;
166
167  views::Label* label_;
168  views::ScrollView* scroll_view_;
169  DesktopMediaListView* list_view_;
170
171  DISALLOW_COPY_AND_ASSIGN(DesktopMediaPickerDialogView);
172};
173
174// Implementation of DesktopMediaPicker for Views.
175class DesktopMediaPickerViews : public DesktopMediaPicker {
176 public:
177  DesktopMediaPickerViews();
178  virtual ~DesktopMediaPickerViews();
179
180  void NotifyDialogResult(DesktopMediaID source);
181
182  // DesktopMediaPicker overrides.
183  virtual void Show(gfx::NativeWindow context,
184                    gfx::NativeWindow parent,
185                    const string16& app_name,
186                    scoped_ptr<DesktopMediaPickerModel> model,
187                    const DoneCallback& done_callback) OVERRIDE;
188
189 private:
190  DoneCallback callback_;
191
192  // The |dialog_| is owned by the corresponding views::Widget instance.
193  // When DesktopMediaPickerViews is destroyed the |dialog_| is destroyed
194  // asynchronously by closing the widget.
195  DesktopMediaPickerDialogView* dialog_;
196
197  DISALLOW_COPY_AND_ASSIGN(DesktopMediaPickerViews);
198};
199
200DesktopMediaSourceView::DesktopMediaSourceView(
201    DesktopMediaListView* parent,
202    DesktopMediaID source_id)
203    : parent_(parent),
204      source_id_(source_id),
205      image_view_(new views::ImageView()),
206      label_(new views::Label()),
207      selected_(false)  {
208  AddChildView(image_view_);
209  AddChildView(label_);
210  set_focusable(true);
211}
212
213DesktopMediaSourceView::~DesktopMediaSourceView() {}
214
215void DesktopMediaSourceView::SetName(const string16& name) {
216  label_->SetText(name);
217}
218
219void DesktopMediaSourceView::SetThumbnail(const gfx::ImageSkia& thumbnail) {
220  image_view_->SetImage(thumbnail);
221}
222
223void DesktopMediaSourceView::SetSelected(bool selected) {
224  if (selected == selected_)
225    return;
226  selected_ = selected;
227
228  if (selected) {
229    // Unselect all other sources.
230    Views neighbours;
231    parent()->GetViewsInGroup(GetGroup(), &neighbours);
232    for (Views::iterator i(neighbours.begin()); i != neighbours.end(); ++i) {
233      if (*i != this) {
234        DCHECK_EQ((*i)->GetClassName(), kDesktopMediaSourceViewClassName);
235        DesktopMediaSourceView* source_view =
236            static_cast<DesktopMediaSourceView*>(*i);
237        source_view->SetSelected(false);
238      }
239    }
240
241    const SkColor bg_color = GetNativeTheme()->GetSystemColor(
242        ui::NativeTheme::kColorId_FocusedMenuItemBackgroundColor);
243    set_background(views::Background::CreateSolidBackground(bg_color));
244
245    parent_->OnSelectionChanged();
246  } else {
247    set_background(NULL);
248  }
249
250  SchedulePaint();
251}
252
253const char* DesktopMediaSourceView::GetClassName() const {
254  return kDesktopMediaSourceViewClassName;
255}
256
257void DesktopMediaSourceView::Layout() {
258  image_view_->SetBounds(kThumbnailMargin, kThumbnailMargin,
259                         kThumbnailWidth, kThumbnailHeight);
260  label_->SetBounds(kThumbnailMargin, kThumbnailHeight + kThumbnailMargin,
261                    kThumbnailWidth, kLabelHeight);
262
263  set_focus_border(views::FocusBorder::CreateDashedFocusBorder(
264      kThumbnailMargin / 2, kThumbnailMargin / 2,
265      kThumbnailMargin / 2, kThumbnailMargin / 2));
266}
267
268views::View* DesktopMediaSourceView::GetSelectedViewForGroup(int group) {
269  Views neighbours;
270  parent()->GetViewsInGroup(group, &neighbours);
271  if (neighbours.empty())
272    return NULL;
273
274  for (Views::iterator i(neighbours.begin()); i != neighbours.end(); ++i) {
275    DCHECK_EQ((*i)->GetClassName(), kDesktopMediaSourceViewClassName);
276    DesktopMediaSourceView* source_view =
277        static_cast<DesktopMediaSourceView*>(*i);
278    if (source_view->selected_)
279      return source_view;
280  }
281  return NULL;
282}
283
284bool DesktopMediaSourceView::IsGroupFocusTraversable() const {
285  return false;
286}
287
288void DesktopMediaSourceView::OnFocus() {
289  View::OnFocus();
290  SetSelected(true);
291  ScrollRectToVisible(gfx::Rect(size()));
292}
293
294bool DesktopMediaSourceView::OnMousePressed(const ui::MouseEvent& event) {
295  RequestFocus();
296  return true;
297}
298
299DesktopMediaListView::DesktopMediaListView(
300    DesktopMediaPickerDialogView* parent,
301    scoped_ptr<DesktopMediaPickerModel> model)
302    : parent_(parent),
303      model_(model.Pass()) {
304  model_->SetThumbnailSize(gfx::Size(kThumbnailWidth, kThumbnailHeight));
305}
306
307DesktopMediaListView::~DesktopMediaListView() {}
308
309void DesktopMediaListView::StartUpdating(
310    content::DesktopMediaID::Id dialog_window_id) {
311  model_->SetViewDialogWindowId(dialog_window_id);
312  model_->StartUpdating(this);
313}
314
315void DesktopMediaListView::OnSelectionChanged() {
316  parent_->OnSelectionChanged();
317}
318
319DesktopMediaSourceView* DesktopMediaListView::GetSelection() {
320  for (int i = 0; i < child_count(); ++i) {
321    DesktopMediaSourceView* source_view =
322        static_cast<DesktopMediaSourceView*>(child_at(i));
323    DCHECK_EQ(source_view->GetClassName(), kDesktopMediaSourceViewClassName);
324    if (source_view->is_selected())
325      return source_view;
326  }
327  return NULL;
328}
329
330gfx::Size DesktopMediaListView::GetPreferredSize() {
331  int total_rows = (child_count() + kListColumns - 1) / kListColumns;
332  return gfx::Size(kTotalListWidth, kListItemHeight * total_rows);
333}
334
335void DesktopMediaListView::Layout() {
336  int x = 0;
337  int y = 0;
338
339  for (int i = 0; i < child_count(); ++i) {
340    if (x + kListItemWidth > kTotalListWidth) {
341      x = 0;
342      y += kListItemHeight;
343    }
344
345    View* source_view = child_at(i);
346    source_view->SetBounds(x, y, kListItemWidth, kListItemHeight);
347
348    x += kListItemWidth;
349  }
350
351  y += kListItemHeight;
352  SetSize(gfx::Size(kTotalListWidth, y));
353}
354
355bool DesktopMediaListView::OnKeyPressed(const ui::KeyEvent& event) {
356  int position_increment = 0;
357  switch (event.key_code()) {
358    case ui::VKEY_UP:
359      position_increment = -kListColumns;
360      break;
361    case ui::VKEY_DOWN:
362      position_increment = kListColumns;
363      break;
364    case ui::VKEY_LEFT:
365      position_increment = -1;
366      break;
367    case ui::VKEY_RIGHT:
368      position_increment = 1;
369      break;
370    default:
371      return false;
372  }
373
374  if (position_increment != 0) {
375    DesktopMediaSourceView* selected = GetSelection();
376    DesktopMediaSourceView* new_selected = NULL;
377
378    if (selected) {
379      int index = GetIndexOf(selected);
380      int new_index = index + position_increment;
381      if (new_index >= child_count())
382        new_index = child_count() - 1;
383      else if (new_index < 0)
384        new_index = 0;
385      if (index != new_index) {
386        new_selected =
387            static_cast<DesktopMediaSourceView*>(child_at(new_index));
388      }
389    } else if (has_children()) {
390      new_selected = static_cast<DesktopMediaSourceView*>(child_at(0));
391    }
392
393    if (new_selected) {
394      GetFocusManager()->SetFocusedView(new_selected);
395    }
396
397    return true;
398  }
399
400  return false;
401}
402
403void DesktopMediaListView::OnSourceAdded(int index) {
404  const DesktopMediaPickerModel::Source& source = model_->source(index);
405  DesktopMediaSourceView* source_view =
406      new DesktopMediaSourceView(this, source.id);
407  source_view->SetName(source.name);
408  source_view->SetGroup(kDesktopMediaSourceViewGroupId);
409  AddChildViewAt(source_view, index);
410
411  PreferredSizeChanged();
412}
413
414void DesktopMediaListView::OnSourceRemoved(int index) {
415  DesktopMediaSourceView* view =
416      static_cast<DesktopMediaSourceView*>(child_at(index));
417  DCHECK(view);
418  DCHECK_EQ(view->GetClassName(), kDesktopMediaSourceViewClassName);
419  bool was_selected = view->is_selected();
420  RemoveChildView(view);
421  delete view;
422
423  if (was_selected)
424    OnSelectionChanged();
425
426  PreferredSizeChanged();
427}
428
429void DesktopMediaListView::OnSourceNameChanged(int index) {
430  const DesktopMediaPickerModel::Source& source = model_->source(index);
431  DesktopMediaSourceView* source_view =
432      static_cast<DesktopMediaSourceView*>(child_at(index));
433  source_view->SetName(source.name);
434}
435
436void DesktopMediaListView::OnSourceThumbnailChanged(int index) {
437  const DesktopMediaPickerModel::Source& source = model_->source(index);
438  DesktopMediaSourceView* source_view =
439      static_cast<DesktopMediaSourceView*>(child_at(index));
440  source_view->SetThumbnail(source.thumbnail);
441}
442
443DesktopMediaPickerDialogView::DesktopMediaPickerDialogView(
444    gfx::NativeWindow context,
445    gfx::NativeWindow parent_window,
446    DesktopMediaPickerViews* parent,
447    const string16& app_name,
448    scoped_ptr<DesktopMediaPickerModel> model)
449    : parent_(parent),
450      app_name_(app_name),
451      label_(new views::Label()),
452      scroll_view_(views::ScrollView::CreateScrollViewWithBorder()),
453      list_view_(new DesktopMediaListView(this, model.Pass())) {
454  label_->SetText(
455      l10n_util::GetStringFUTF16(IDS_DESKTOP_MEDIA_PICKER_TEXT, app_name_));
456  label_->SetMultiLine(true);
457  label_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
458  AddChildView(label_);
459
460  scroll_view_->SetContents(list_view_);
461  AddChildView(scroll_view_);
462
463  DialogDelegate::CreateDialogWidget(this, context, parent_window);
464
465  // DesktopMediaPickerModel needs to know the ID of the picker window which
466  // matches the ID it gets from the OS. Depending on the OS and configuration
467  // we get this ID differently.
468  //
469  // TODO(sergeyu): Update this code when Ash-specific window capturer is
470  // implemented. Currently this code will always get native windows ID
471  // which is not what we need in Ash. http://crbug.com/295102
472  content::DesktopMediaID::Id dialog_window_id;
473
474#if defined(OS_WIN)
475  dialog_window_id = reinterpret_cast<content::DesktopMediaID::Id>(
476      views::HWNDForWidget(GetWidget()));
477#elif defined(USE_AURA)
478  dialog_window_id = static_cast<content::DesktopMediaID::Id>(
479      GetWidget()->GetNativeWindow()->GetRootWindow()->GetAcceleratedWidget());
480#else
481  dialog_window_id = 0;
482  NOTIMPLEMENTED();
483#endif
484
485  list_view_->StartUpdating(dialog_window_id);
486
487  GetWidget()->Show();
488}
489
490DesktopMediaPickerDialogView::~DesktopMediaPickerDialogView() {}
491
492void DesktopMediaPickerDialogView::DetachParent() {
493  parent_ = NULL;
494}
495
496gfx::Size DesktopMediaPickerDialogView::GetPreferredSize() {
497  return gfx::Size(600, 500);
498}
499
500void DesktopMediaPickerDialogView::Layout() {
501  gfx::Rect rect = GetLocalBounds();
502  rect.Inset(views::kPanelHorizMargin, views::kPanelVertMargin);
503
504  gfx::Rect label_rect(rect.x(), rect.y(), rect.width(),
505                       label_->GetHeightForWidth(rect.width()));
506  label_->SetBoundsRect(label_rect);
507
508  int scroll_view_top = label_rect.bottom() + views::kPanelVerticalSpacing;
509  scroll_view_->SetBounds(
510      rect.x(), scroll_view_top,
511      rect.width(), rect.height() - scroll_view_top);
512}
513
514base::string16 DesktopMediaPickerDialogView::GetWindowTitle() const {
515  return l10n_util::GetStringFUTF16(IDS_DESKTOP_MEDIA_PICKER_TITLE, app_name_);
516}
517
518bool DesktopMediaPickerDialogView::IsDialogButtonEnabled(
519    ui::DialogButton button) const {
520  if (button == ui::DIALOG_BUTTON_OK)
521    return list_view_->GetSelection() != NULL;
522  return true;
523}
524
525bool DesktopMediaPickerDialogView::Accept() {
526  DesktopMediaSourceView* selection = list_view_->GetSelection();
527
528  // Ok button should only be enabled when a source is selected.
529  DCHECK(selection);
530
531  DesktopMediaID source;
532  if (selection)
533    source = selection->source_id();
534
535  if (parent_)
536    parent_->NotifyDialogResult(source);
537
538  // Return true to close the window.
539  return true;
540}
541
542void DesktopMediaPickerDialogView::DeleteDelegate() {
543  // If the dialog is being closed then notify the parent about it.
544  if (parent_)
545    parent_->NotifyDialogResult(DesktopMediaID());
546  delete this;
547}
548
549void DesktopMediaPickerDialogView::OnSelectionChanged() {
550  GetDialogClientView()->UpdateDialogButtons();
551}
552
553DesktopMediaPickerViews::DesktopMediaPickerViews()
554    : dialog_(NULL) {
555}
556
557DesktopMediaPickerViews::~DesktopMediaPickerViews() {
558  if (dialog_) {
559    dialog_->DetachParent();
560    dialog_->GetWidget()->Close();
561  }
562}
563
564void DesktopMediaPickerViews::Show(gfx::NativeWindow context,
565                                   gfx::NativeWindow parent,
566                                   const string16& app_name,
567                                   scoped_ptr<DesktopMediaPickerModel> model,
568                                   const DoneCallback& done_callback) {
569  callback_ = done_callback;
570  dialog_ = new DesktopMediaPickerDialogView(
571      context, parent, this, app_name, model.Pass());
572}
573
574void DesktopMediaPickerViews::NotifyDialogResult(
575    DesktopMediaID source) {
576  // Once this method is called the |dialog_| will close and destroy itself.
577  dialog_->DetachParent();
578  dialog_ = NULL;
579
580  DCHECK(!callback_.is_null());
581
582  // Notify the |callback_| asynchronously because it may need to destroy
583  // DesktopMediaPicker.
584  content::BrowserThread::PostTask(
585      content::BrowserThread::UI, FROM_HERE,
586      base::Bind(callback_, source));
587  callback_.Reset();
588}
589
590}  // namespace
591
592// static
593scoped_ptr<DesktopMediaPicker> DesktopMediaPicker::Create() {
594  return scoped_ptr<DesktopMediaPicker>(new DesktopMediaPickerViews());
595}
596