desktop_media_picker_views.cc revision 58537e28ecd584eab876aee8be7156509866d23a
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
26using content::DesktopMediaID;
27
28namespace {
29
30const int kThumbnailWidth = 160;
31const int kThumbnailHeight = 100;
32const int kThumbnailMargin = 10;
33const int kLabelHeight = 40;
34const int kListItemWidth = kThumbnailMargin * 2 + kThumbnailWidth;
35const int kListItemHeight =
36    kThumbnailMargin * 2 + kThumbnailHeight + kLabelHeight;
37const int kListColumns = 3;
38const int kTotalListWidth = kListColumns * kListItemWidth;
39
40const int kDesktopMediaSourceViewGroupId = 1;
41
42const char kDesktopMediaSourceViewClassName[] =
43    "DesktopMediaPicker_DesktopMediaSourceView";
44
45class DesktopMediaListView;
46class DesktopMediaPickerDialogView;
47class 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.
51class 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.
96class 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.
128class 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.
165class 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
190DesktopMediaSourceView::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
203DesktopMediaSourceView::~DesktopMediaSourceView() {}
204
205void DesktopMediaSourceView::SetName(const string16& name) {
206  label_->SetText(name);
207}
208
209void DesktopMediaSourceView::SetThumbnail(const gfx::ImageSkia& thumbnail) {
210  image_view_->SetImage(thumbnail);
211}
212
213void 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
243const char* DesktopMediaSourceView::GetClassName() const {
244  return kDesktopMediaSourceViewClassName;
245}
246
247void 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
258views::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
274bool DesktopMediaSourceView::IsGroupFocusTraversable() const {
275  return false;
276}
277
278void DesktopMediaSourceView::OnFocus() {
279  View::OnFocus();
280  SetSelected(true);
281  ScrollRectToVisible(gfx::Rect(size()));
282}
283
284bool DesktopMediaSourceView::OnMousePressed(const ui::MouseEvent& event) {
285  RequestFocus();
286  return true;
287}
288
289DesktopMediaListView::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
298DesktopMediaListView::~DesktopMediaListView() {
299}
300
301void DesktopMediaListView::OnSelectionChanged() {
302  parent_->OnSelectionChanged();
303}
304
305DesktopMediaSourceView* 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
316gfx::Size DesktopMediaListView::GetPreferredSize() {
317  int total_rows = (child_count() + kListColumns - 1) / kListColumns;
318  return gfx::Size(kTotalListWidth, kListItemHeight * total_rows);
319}
320
321void 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  y += kListItemHeight;
338  SetSize(gfx::Size(kTotalListWidth, y));
339}
340
341bool DesktopMediaListView::OnKeyPressed(const ui::KeyEvent& event) {
342  int position_increment = 0;
343  switch (event.key_code()) {
344    case ui::VKEY_UP:
345      position_increment = -kListColumns;
346      break;
347    case ui::VKEY_DOWN:
348      position_increment = kListColumns;
349      break;
350    case ui::VKEY_LEFT:
351      position_increment = -1;
352      break;
353    case ui::VKEY_RIGHT:
354      position_increment = 1;
355      break;
356    default:
357      return false;
358  }
359
360  if (position_increment != 0) {
361    DesktopMediaSourceView* selected = GetSelection();
362    DesktopMediaSourceView* new_selected = NULL;
363
364    if (selected) {
365      int index = GetIndexOf(selected);
366      int new_index = index + position_increment;
367      if (new_index >= child_count())
368        new_index = child_count() - 1;
369      else if (new_index < 0)
370        new_index = 0;
371      if (index != new_index) {
372        new_selected =
373            static_cast<DesktopMediaSourceView*>(child_at(new_index));
374      }
375    } else if (has_children()) {
376      new_selected = static_cast<DesktopMediaSourceView*>(child_at(0));
377    }
378
379    if (new_selected) {
380      GetFocusManager()->SetFocusedView(new_selected);
381    }
382
383    return true;
384  }
385
386  return false;
387}
388
389void DesktopMediaListView::OnSourceAdded(int index) {
390  const DesktopMediaPickerModel::Source& source = model_->source(index);
391  DesktopMediaSourceView* source_view =
392      new DesktopMediaSourceView(this, source.id);
393  source_view->SetName(source.name);
394  source_view->SetGroup(kDesktopMediaSourceViewGroupId);
395  AddChildViewAt(source_view, index);
396
397  PreferredSizeChanged();
398}
399
400void DesktopMediaListView::OnSourceRemoved(int index) {
401  DesktopMediaSourceView* view =
402      static_cast<DesktopMediaSourceView*>(child_at(index));
403  DCHECK(view);
404  DCHECK_EQ(view->GetClassName(), kDesktopMediaSourceViewClassName);
405  bool was_selected = view->is_selected();
406  RemoveChildView(view);
407  delete view;
408
409  if (was_selected)
410    OnSelectionChanged();
411
412  PreferredSizeChanged();
413}
414
415void DesktopMediaListView::OnSourceNameChanged(int index) {
416  const DesktopMediaPickerModel::Source& source = model_->source(index);
417  DesktopMediaSourceView* source_view =
418      static_cast<DesktopMediaSourceView*>(child_at(index));
419  source_view->SetName(source.name);
420}
421
422void DesktopMediaListView::OnSourceThumbnailChanged(int index) {
423  const DesktopMediaPickerModel::Source& source = model_->source(index);
424  DesktopMediaSourceView* source_view =
425      static_cast<DesktopMediaSourceView*>(child_at(index));
426  source_view->SetThumbnail(source.thumbnail);
427}
428
429DesktopMediaPickerDialogView::DesktopMediaPickerDialogView(
430    gfx::NativeWindow context,
431    gfx::NativeWindow parent_window,
432    DesktopMediaPickerViews* parent,
433    const string16& app_name,
434    scoped_ptr<DesktopMediaPickerModel> model)
435    : parent_(parent),
436      app_name_(app_name),
437      label_(new views::Label()),
438      scroll_view_(views::ScrollView::CreateScrollViewWithBorder()),
439      list_view_(new DesktopMediaListView(this, model.Pass())) {
440  label_->SetText(
441      l10n_util::GetStringFUTF16(IDS_DESKTOP_MEDIA_PICKER_TEXT, app_name_));
442  label_->SetMultiLine(true);
443  label_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
444  AddChildView(label_);
445
446  scroll_view_->SetContents(list_view_);
447  AddChildView(scroll_view_);
448
449  DialogDelegate::CreateDialogWidget(this, context, parent_window);
450  GetWidget()->Show();
451}
452
453DesktopMediaPickerDialogView::~DesktopMediaPickerDialogView() {}
454
455void DesktopMediaPickerDialogView::DetachParent() {
456  parent_ = NULL;
457}
458
459gfx::Size DesktopMediaPickerDialogView::GetPreferredSize() {
460  return gfx::Size(600, 500);
461}
462
463void DesktopMediaPickerDialogView::Layout() {
464  gfx::Rect rect = GetLocalBounds();
465  rect.Inset(views::kPanelHorizMargin, views::kPanelVertMargin);
466
467  gfx::Rect label_rect(rect.x(), rect.y(), rect.width(),
468                       label_->GetHeightForWidth(rect.width()));
469  label_->SetBoundsRect(label_rect);
470
471  int scroll_view_top = label_rect.bottom() + views::kPanelVerticalSpacing;
472  scroll_view_->SetBounds(
473      rect.x(), scroll_view_top,
474      rect.width(), rect.height() - scroll_view_top);
475}
476
477base::string16 DesktopMediaPickerDialogView::GetWindowTitle() const {
478  return l10n_util::GetStringFUTF16(IDS_DESKTOP_MEDIA_PICKER_TITLE, app_name_);
479}
480
481bool DesktopMediaPickerDialogView::IsDialogButtonEnabled(
482    ui::DialogButton button) const {
483  if (button == ui::DIALOG_BUTTON_OK)
484    return list_view_->GetSelection() != NULL;
485  return true;
486}
487
488bool DesktopMediaPickerDialogView::Accept() {
489  DesktopMediaSourceView* selection = list_view_->GetSelection();
490
491  // Ok button should only be enabled when a source is selected.
492  DCHECK(selection);
493
494  DesktopMediaID source;
495  if (selection)
496    source = selection->source_id();
497
498  if (parent_)
499    parent_->NotifyDialogResult(source);
500
501  // Return true to close the window.
502  return true;
503}
504
505void DesktopMediaPickerDialogView::DeleteDelegate() {
506  // If the dialog is being closed then notify the parent about it.
507  if (parent_)
508    parent_->NotifyDialogResult(DesktopMediaID());
509  delete this;
510}
511
512void DesktopMediaPickerDialogView::OnSelectionChanged() {
513  GetDialogClientView()->UpdateDialogButtons();
514}
515
516DesktopMediaPickerViews::DesktopMediaPickerViews()
517    : dialog_(NULL) {
518}
519
520DesktopMediaPickerViews::~DesktopMediaPickerViews() {
521  if (dialog_) {
522    dialog_->DetachParent();
523    dialog_->GetWidget()->Close();
524  }
525}
526
527void DesktopMediaPickerViews::Show(gfx::NativeWindow context,
528                                   gfx::NativeWindow parent,
529                                   const string16& app_name,
530                                   scoped_ptr<DesktopMediaPickerModel> model,
531                                   const DoneCallback& done_callback) {
532  callback_ = done_callback;
533  dialog_ = new DesktopMediaPickerDialogView(
534      context, parent, this, app_name, model.Pass());
535}
536
537void DesktopMediaPickerViews::NotifyDialogResult(
538    DesktopMediaID source) {
539  // Once this method is called the |dialog_| will close and destroy itself.
540  dialog_->DetachParent();
541  dialog_ = NULL;
542
543  DCHECK(!callback_.is_null());
544
545  // Notify the |callback_| asynchronously because it may need to destroy
546  // DesktopMediaPicker.
547  content::BrowserThread::PostTask(
548      content::BrowserThread::UI, FROM_HERE,
549      base::Bind(callback_, source));
550  callback_.Reset();
551}
552
553}  // namespace
554
555// static
556scoped_ptr<DesktopMediaPicker> DesktopMediaPicker::Create() {
557  return scoped_ptr<DesktopMediaPicker>(new DesktopMediaPickerViews());
558}
559