desktop_media_picker_views.cc revision a36e5920737c6adbddd3e43b760e5de8431db6e0
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
26namespace {
27
28const int kThumbnailWidth = 160;
29const int kThumbnailHeight = 100;
30const int kThumbnailMargin = 10;
31const int kLabelHeight = 40;
32const int kListItemWidth = kThumbnailMargin * 2 + kThumbnailWidth;
33const int kListItemHeight =
34    kThumbnailMargin * 2 + kThumbnailHeight + kLabelHeight;
35const int kListColumns = 3;
36const int kTotalListWidth = kListColumns * kListItemWidth;
37
38const int kDesktopMediaSourceViewGroupId = 1;
39
40const char kDesktopMediaSourceViewClassName[] =
41    "DesktopMediaPicker_DesktopMediaSourceView";
42
43class DesktopMediaListView;
44class DesktopMediaPickerDialogView;
45class DesktopMediaPickerViews;
46
47// View used for each item in DesktopMediaListView. Shows a single desktop media
48// source as a thumbnail with the title under it.
49class DesktopMediaSourceView : public views::View {
50 public:
51  DesktopMediaSourceView(DesktopMediaListView* parent,
52                         DesktopMediaPickerModel::SourceId source_id);
53  virtual ~DesktopMediaSourceView();
54
55  // Updates thumbnail and title from |source|.
56  void SetName(const string16& name);
57  void SetThumbnail(const gfx::ImageSkia& thumbnail);
58
59  // Id for the source shown by this View.
60  const DesktopMediaPickerModel::SourceId& source_id() const {
61    return source_id_;
62  }
63
64  // Returns true if the source is selected.
65  bool is_selected() const { return selected_; }
66
67  // Updates selection state of the element. If |selected| is true then also
68  // calls SetSelected(false) for the source view that was selected before that
69  // (if any).
70  void SetSelected(bool selected);
71
72  // views::View interface.
73  virtual const char* GetClassName() const OVERRIDE;
74  virtual void Layout() OVERRIDE;
75  virtual views::View* GetSelectedViewForGroup(int group) OVERRIDE;
76  virtual bool IsGroupFocusTraversable() const OVERRIDE;
77  virtual void OnFocus() OVERRIDE;
78  virtual bool OnMousePressed(const ui::MouseEvent& event) OVERRIDE;
79
80 private:
81  DesktopMediaListView* parent_;
82
83  DesktopMediaPickerModel::SourceId source_id_;
84
85  views::ImageView* image_view_;
86  views::Label* label_;
87
88  bool selected_;
89
90  DISALLOW_COPY_AND_ASSIGN(DesktopMediaSourceView);
91};
92
93// View that shows list of desktop media sources available from
94// DesktopMediaPickerModel.
95class DesktopMediaListView : public views::View,
96                             public DesktopMediaPickerModel::Observer {
97 public:
98  DesktopMediaListView(DesktopMediaPickerDialogView* parent,
99                       scoped_ptr<DesktopMediaPickerModel> model);
100  virtual ~DesktopMediaListView();
101
102  // Called by DesktopMediaSourceView when selection has changed.
103  void OnSelectionChanged();
104
105  // Returns currently selected source.
106  DesktopMediaSourceView* GetSelection();
107
108  // views::View overrides.
109  virtual gfx::Size GetPreferredSize() OVERRIDE;
110  virtual void Layout() OVERRIDE;
111  virtual bool OnKeyPressed(const ui::KeyEvent& event) OVERRIDE;
112
113 private:
114  // DesktopMediaPickerModel::Observer interface
115  virtual void OnSourceAdded(int index) OVERRIDE;
116  virtual void OnSourceRemoved(int index) OVERRIDE;
117  virtual void OnSourceNameChanged(int index) OVERRIDE;
118  virtual void OnSourceThumbnailChanged(int index) OVERRIDE;
119
120  DesktopMediaPickerDialogView* parent_;
121  scoped_ptr<DesktopMediaPickerModel> model_;
122
123  DISALLOW_COPY_AND_ASSIGN(DesktopMediaListView);
124};
125
126// Dialog view used for DesktopMediaPickerViews.
127class DesktopMediaPickerDialogView : public views::DialogDelegateView {
128 public:
129  DesktopMediaPickerDialogView(gfx::NativeWindow context,
130                               gfx::NativeWindow parent_window,
131                               DesktopMediaPickerViews* parent,
132                               const string16& app_name,
133                               scoped_ptr<DesktopMediaPickerModel> model);
134  virtual ~DesktopMediaPickerDialogView();
135
136  // Called by parent (DesktopMediaPickerViews) when it's destroyed.
137  void DetachParent();
138
139  // Called by DesktopMediaListView.
140  void OnSelectionChanged();
141
142  // views::View overrides.
143  virtual gfx::Size GetPreferredSize() OVERRIDE;
144  virtual void Layout() OVERRIDE;
145
146  // views::DialogDelegateView overrides.
147  virtual base::string16 GetWindowTitle() const OVERRIDE;
148  virtual bool IsDialogButtonEnabled(ui::DialogButton button) const OVERRIDE;
149  virtual bool Accept() OVERRIDE;
150  virtual void DeleteDelegate() OVERRIDE;
151
152 private:
153  DesktopMediaPickerViews* parent_;
154  string16 app_name_;
155
156  views::Label* label_;
157  views::ScrollView* scroll_view_;
158  DesktopMediaListView* list_view_;
159
160  DISALLOW_COPY_AND_ASSIGN(DesktopMediaPickerDialogView);
161};
162
163// Implementation of DesktopMediaPicker for Views.
164class DesktopMediaPickerViews : public DesktopMediaPicker {
165 public:
166  DesktopMediaPickerViews();
167  virtual ~DesktopMediaPickerViews();
168
169  void NotifyDialogResult(DesktopMediaPickerModel::SourceId source);
170
171  // DesktopMediaPicker overrides.
172  virtual void Show(gfx::NativeWindow context,
173                    gfx::NativeWindow parent,
174                    const string16& app_name,
175                    scoped_ptr<DesktopMediaPickerModel> model,
176                    const DoneCallback& done_callback) OVERRIDE;
177
178 private:
179  DoneCallback callback_;
180
181  // The |dialog_| is owned by the corresponding views::Widget instance.
182  // When DesktopMediaPickerViews is destroyed the |dialog_| is destroyed
183  // asynchronously by closing the widget.
184  DesktopMediaPickerDialogView* dialog_;
185
186  DISALLOW_COPY_AND_ASSIGN(DesktopMediaPickerViews);
187};
188
189DesktopMediaSourceView::DesktopMediaSourceView(
190    DesktopMediaListView* parent,
191    DesktopMediaPickerModel::SourceId source_id)
192    : parent_(parent),
193      source_id_(source_id),
194      image_view_(new views::ImageView()),
195      label_(new views::Label()),
196      selected_(false)  {
197  AddChildView(image_view_);
198  AddChildView(label_);
199  set_focusable(true);
200}
201
202DesktopMediaSourceView::~DesktopMediaSourceView() {}
203
204void DesktopMediaSourceView::SetName(const string16& name) {
205  label_->SetText(name);
206}
207
208void DesktopMediaSourceView::SetThumbnail(const gfx::ImageSkia& thumbnail) {
209  image_view_->SetImage(thumbnail);
210}
211
212void DesktopMediaSourceView::SetSelected(bool selected) {
213  if (selected == selected_)
214    return;
215  selected_ = selected;
216
217  if (selected) {
218    // Unselect all other sources.
219    Views neighbours;
220    parent()->GetViewsInGroup(GetGroup(), &neighbours);
221    for (Views::iterator i(neighbours.begin()); i != neighbours.end(); ++i) {
222      if (*i != this) {
223        DCHECK_EQ((*i)->GetClassName(), kDesktopMediaSourceViewClassName);
224        DesktopMediaSourceView* source_view =
225            static_cast<DesktopMediaSourceView*>(*i);
226        source_view->SetSelected(false);
227      }
228    }
229
230    const SkColor bg_color = GetNativeTheme()->GetSystemColor(
231        ui::NativeTheme::kColorId_FocusedMenuItemBackgroundColor);
232    set_background(views::Background::CreateSolidBackground(bg_color));
233
234    parent_->OnSelectionChanged();
235  } else {
236    set_background(NULL);
237  }
238
239  SchedulePaint();
240}
241
242const char* DesktopMediaSourceView::GetClassName() const {
243  return kDesktopMediaSourceViewClassName;
244}
245
246void DesktopMediaSourceView::Layout() {
247  image_view_->SetBounds(kThumbnailMargin, kThumbnailMargin,
248                         kThumbnailWidth, kThumbnailHeight);
249  label_->SetBounds(kThumbnailMargin, kThumbnailHeight + kThumbnailMargin,
250                    kThumbnailWidth, kLabelHeight);
251
252  set_focus_border(views::FocusBorder::CreateDashedFocusBorder(
253      kThumbnailMargin / 2, kThumbnailMargin / 2,
254      kThumbnailMargin / 2, kThumbnailMargin / 2));
255}
256
257views::View* DesktopMediaSourceView::GetSelectedViewForGroup(int group) {
258  Views neighbours;
259  parent()->GetViewsInGroup(group, &neighbours);
260  if (neighbours.empty())
261    return NULL;
262
263  for (Views::iterator i(neighbours.begin()); i != neighbours.end(); ++i) {
264    DCHECK_EQ((*i)->GetClassName(), kDesktopMediaSourceViewClassName);
265    DesktopMediaSourceView* source_view =
266        static_cast<DesktopMediaSourceView*>(*i);
267    if (source_view->selected_)
268      return source_view;
269  }
270  return NULL;
271}
272
273bool DesktopMediaSourceView::IsGroupFocusTraversable() const {
274  return false;
275}
276
277void DesktopMediaSourceView::OnFocus() {
278  View::OnFocus();
279  SetSelected(true);
280  ScrollRectToVisible(gfx::Rect(size()));
281}
282
283bool DesktopMediaSourceView::OnMousePressed(const ui::MouseEvent& event) {
284  RequestFocus();
285  return true;
286}
287
288DesktopMediaListView::DesktopMediaListView(
289    DesktopMediaPickerDialogView* parent,
290    scoped_ptr<DesktopMediaPickerModel> model)
291    : parent_(parent),
292      model_(model.Pass()) {
293  model_->SetThumbnailSize(gfx::Size(kThumbnailWidth, kThumbnailHeight));
294  model_->StartUpdating(this);
295}
296
297DesktopMediaListView::~DesktopMediaListView() {
298}
299
300void DesktopMediaListView::OnSelectionChanged() {
301  parent_->OnSelectionChanged();
302}
303
304DesktopMediaSourceView* DesktopMediaListView::GetSelection() {
305  for (int i = 0; i < child_count(); ++i) {
306    DesktopMediaSourceView* source_view =
307        static_cast<DesktopMediaSourceView*>(child_at(i));
308    DCHECK_EQ(source_view->GetClassName(), kDesktopMediaSourceViewClassName);
309    if (source_view->is_selected())
310      return source_view;
311  }
312  return NULL;
313}
314
315gfx::Size DesktopMediaListView::GetPreferredSize() {
316  int total_rows = (child_count() + kListColumns - 1) / kListColumns;
317  return gfx::Size(kTotalListWidth, kListItemHeight * total_rows);
318}
319
320void DesktopMediaListView::Layout() {
321  int x = 0;
322  int y = 0;
323
324  for (int i = 0; i < child_count(); ++i) {
325    if (x + kListItemWidth > kTotalListWidth) {
326      x = 0;
327      y += kListItemHeight;
328    }
329
330    View* source_view = child_at(i);
331    source_view->SetBounds(x, y, kListItemWidth, kListItemHeight);
332
333    x += kListItemWidth;
334  }
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
361  if (position_increment != 0) {
362    DesktopMediaSourceView* selected = GetSelection();
363    DesktopMediaSourceView* new_selected = NULL;
364
365    if (selected) {
366      int index = GetIndexOf(selected);
367      int new_index = index + position_increment;
368      if (new_index >= child_count())
369        new_index = child_count() - 1;
370      else if (new_index < 0)
371        new_index = 0;
372      if (index != new_index) {
373        new_selected =
374            static_cast<DesktopMediaSourceView*>(child_at(new_index));
375      }
376    } else if (has_children()) {
377      new_selected = static_cast<DesktopMediaSourceView*>(child_at(0));
378    }
379
380    if (new_selected) {
381      GetFocusManager()->SetFocusedView(new_selected);
382    }
383
384    return true;
385  }
386
387  return false;
388}
389
390void DesktopMediaListView::OnSourceAdded(int index) {
391  const DesktopMediaPickerModel::Source& source = model_->source(index);
392  DesktopMediaSourceView* source_view =
393      new DesktopMediaSourceView(this, source.id);
394  source_view->SetName(source.name);
395  source_view->SetGroup(kDesktopMediaSourceViewGroupId);
396  AddChildViewAt(source_view, index);
397  Layout();
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  DesktopMediaPickerModel::SourceId 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(
509        DesktopMediaPickerModel::SourceId(content::MEDIA_NO_SERVICE, 0));
510  }
511  delete this;
512}
513
514void DesktopMediaPickerDialogView::OnSelectionChanged() {
515  GetDialogClientView()->UpdateDialogButtons();
516}
517
518DesktopMediaPickerViews::DesktopMediaPickerViews()
519    : dialog_(NULL) {
520}
521
522DesktopMediaPickerViews::~DesktopMediaPickerViews() {
523  if (dialog_) {
524    dialog_->DetachParent();
525    dialog_->GetWidget()->Close();
526  }
527}
528
529void DesktopMediaPickerViews::Show(gfx::NativeWindow context,
530                                   gfx::NativeWindow parent,
531                                   const string16& app_name,
532                                   scoped_ptr<DesktopMediaPickerModel> model,
533                                   const DoneCallback& done_callback) {
534  callback_ = done_callback;
535  dialog_ = new DesktopMediaPickerDialogView(
536      context, parent, this, app_name, model.Pass());
537}
538
539void DesktopMediaPickerViews::NotifyDialogResult(
540    DesktopMediaPickerModel::SourceId source) {
541  // Once this method is called the |dialog_| will close and destroy itself.
542  dialog_->DetachParent();
543  dialog_ = NULL;
544
545  DCHECK(!callback_.is_null());
546
547  // Notify the |callback_| asynchronously because it may need to destroy
548  // DesktopMediaPicker.
549  content::BrowserThread::PostTask(
550      content::BrowserThread::UI, FROM_HERE,
551      base::Bind(callback_, source));
552  callback_.Reset();
553}
554
555}  // namespace
556
557// static
558scoped_ptr<DesktopMediaPicker> DesktopMediaPicker::Create() {
559  return scoped_ptr<DesktopMediaPicker>(new DesktopMediaPickerViews());
560}
561