desktop_media_picker_views.cc revision 1320f92c476a1ad9d19dba2a48c72b75566198e9
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/ui/views/desktop_media_picker_views.h"
6
7#include "base/callback.h"
8#include "base/command_line.h"
9#include "base/strings/utf_string_conversions.h"
10#include "chrome/browser/media/desktop_media_list.h"
11#include "chrome/browser/ui/ash/ash_util.h"
12#include "chrome/browser/ui/views/constrained_window_views.h"
13#include "chrome/common/chrome_switches.h"
14#include "chrome/grit/generated_resources.h"
15#include "components/web_modal/popup_manager.h"
16#include "components/web_modal/web_contents_modal_dialog_manager.h"
17#include "content/public/browser/browser_thread.h"
18#include "content/public/browser/web_contents_delegate.h"
19#include "ui/aura/window_tree_host.h"
20#include "ui/base/l10n/l10n_util.h"
21#include "ui/events/event_constants.h"
22#include "ui/events/keycodes/keyboard_codes.h"
23#include "ui/gfx/canvas.h"
24#include "ui/native_theme/native_theme.h"
25#include "ui/views/background.h"
26#include "ui/views/bubble/bubble_frame_view.h"
27#include "ui/views/controls/image_view.h"
28#include "ui/views/controls/label.h"
29#include "ui/views/controls/scroll_view.h"
30#include "ui/views/layout/layout_constants.h"
31#include "ui/views/widget/widget.h"
32#include "ui/views/window/dialog_client_view.h"
33#include "ui/wm/core/shadow_types.h"
34
35using content::DesktopMediaID;
36
37namespace {
38
39const int kThumbnailWidth = 160;
40const int kThumbnailHeight = 100;
41const int kThumbnailMargin = 10;
42const int kLabelHeight = 40;
43const int kListItemWidth = kThumbnailMargin * 2 + kThumbnailWidth;
44const int kListItemHeight =
45    kThumbnailMargin * 2 + kThumbnailHeight + kLabelHeight;
46const int kListColumns = 3;
47const int kTotalListWidth = kListColumns * kListItemWidth;
48
49const int kDesktopMediaSourceViewGroupId = 1;
50
51const char kDesktopMediaSourceViewClassName[] =
52    "DesktopMediaPicker_DesktopMediaSourceView";
53
54DesktopMediaID::Id AcceleratedWidgetToDesktopMediaId(
55    gfx::AcceleratedWidget accelerated_widget) {
56#if defined(OS_WIN)
57  return reinterpret_cast<DesktopMediaID::Id>(accelerated_widget);
58#else
59  return static_cast<DesktopMediaID::Id>(accelerated_widget);
60#endif
61}
62
63int GetMediaListViewHeightForRows(size_t rows) {
64  return kListItemHeight * rows;
65}
66
67}  // namespace
68
69DesktopMediaSourceView::DesktopMediaSourceView(
70    DesktopMediaListView* parent,
71    DesktopMediaID source_id)
72    : parent_(parent),
73      source_id_(source_id),
74      image_view_(new views::ImageView()),
75      label_(new views::Label()),
76      selected_(false)  {
77  AddChildView(image_view_);
78  AddChildView(label_);
79  SetFocusable(true);
80}
81
82DesktopMediaSourceView::~DesktopMediaSourceView() {}
83
84void DesktopMediaSourceView::SetName(const base::string16& name) {
85  label_->SetText(name);
86}
87
88void DesktopMediaSourceView::SetThumbnail(const gfx::ImageSkia& thumbnail) {
89  image_view_->SetImage(thumbnail);
90}
91
92void DesktopMediaSourceView::SetSelected(bool selected) {
93  if (selected == selected_)
94    return;
95  selected_ = selected;
96
97  if (selected) {
98    // Unselect all other sources.
99    Views neighbours;
100    parent()->GetViewsInGroup(GetGroup(), &neighbours);
101    for (Views::iterator i(neighbours.begin()); i != neighbours.end(); ++i) {
102      if (*i != this) {
103        DCHECK_EQ((*i)->GetClassName(), kDesktopMediaSourceViewClassName);
104        DesktopMediaSourceView* source_view =
105            static_cast<DesktopMediaSourceView*>(*i);
106        source_view->SetSelected(false);
107      }
108    }
109
110    const SkColor bg_color = GetNativeTheme()->GetSystemColor(
111        ui::NativeTheme::kColorId_FocusedMenuItemBackgroundColor);
112    set_background(views::Background::CreateSolidBackground(bg_color));
113
114    parent_->OnSelectionChanged();
115  } else {
116    set_background(NULL);
117  }
118
119  SchedulePaint();
120}
121
122const char* DesktopMediaSourceView::GetClassName() const {
123  return kDesktopMediaSourceViewClassName;
124}
125
126void DesktopMediaSourceView::Layout() {
127  image_view_->SetBounds(kThumbnailMargin, kThumbnailMargin,
128                         kThumbnailWidth, kThumbnailHeight);
129  label_->SetBounds(kThumbnailMargin, kThumbnailHeight + kThumbnailMargin,
130                    kThumbnailWidth, kLabelHeight);
131}
132
133views::View* DesktopMediaSourceView::GetSelectedViewForGroup(int group) {
134  Views neighbours;
135  parent()->GetViewsInGroup(group, &neighbours);
136  if (neighbours.empty())
137    return NULL;
138
139  for (Views::iterator i(neighbours.begin()); i != neighbours.end(); ++i) {
140    DCHECK_EQ((*i)->GetClassName(), kDesktopMediaSourceViewClassName);
141    DesktopMediaSourceView* source_view =
142        static_cast<DesktopMediaSourceView*>(*i);
143    if (source_view->selected_)
144      return source_view;
145  }
146  return NULL;
147}
148
149bool DesktopMediaSourceView::IsGroupFocusTraversable() const {
150  return false;
151}
152
153void DesktopMediaSourceView::OnPaint(gfx::Canvas* canvas) {
154  View::OnPaint(canvas);
155  if (HasFocus()) {
156    gfx::Rect bounds(GetLocalBounds());
157    bounds.Inset(kThumbnailMargin / 2, kThumbnailMargin / 2);
158    canvas->DrawFocusRect(bounds);
159  }
160}
161
162void DesktopMediaSourceView::OnFocus() {
163  View::OnFocus();
164  SetSelected(true);
165  ScrollRectToVisible(gfx::Rect(size()));
166  // We paint differently when focused.
167  SchedulePaint();
168}
169
170void DesktopMediaSourceView::OnBlur() {
171  View::OnBlur();
172  // We paint differently when focused.
173  SchedulePaint();
174}
175
176bool DesktopMediaSourceView::OnMousePressed(const ui::MouseEvent& event) {
177  if (event.GetClickCount() == 1) {
178    RequestFocus();
179  } else if (event.GetClickCount() == 2) {
180    RequestFocus();
181    parent_->OnDoubleClick();
182  }
183  return true;
184}
185
186void DesktopMediaSourceView::OnGestureEvent(ui::GestureEvent* event) {
187  if (event->type() == ui::ET_GESTURE_TAP &&
188      event->details().tap_count() == 2) {
189    RequestFocus();
190    parent_->OnDoubleClick();
191    event->SetHandled();
192    return;
193  }
194
195  // Detect tap gesture using ET_GESTURE_TAP_DOWN so the view also gets focused
196  // on the long tap (when the tap gesture starts).
197  if (event->type() == ui::ET_GESTURE_TAP_DOWN) {
198    RequestFocus();
199    event->SetHandled();
200  }
201}
202
203DesktopMediaListView::DesktopMediaListView(
204    DesktopMediaPickerDialogView* parent,
205    scoped_ptr<DesktopMediaList> media_list)
206    : parent_(parent),
207      media_list_(media_list.Pass()),
208      weak_factory_(this) {
209  media_list_->SetThumbnailSize(gfx::Size(kThumbnailWidth, kThumbnailHeight));
210}
211
212DesktopMediaListView::~DesktopMediaListView() {}
213
214void DesktopMediaListView::StartUpdating(DesktopMediaID::Id dialog_window_id) {
215  media_list_->SetViewDialogWindowId(dialog_window_id);
216  media_list_->StartUpdating(this);
217}
218
219void DesktopMediaListView::OnSelectionChanged() {
220  parent_->OnSelectionChanged();
221}
222
223void DesktopMediaListView::OnDoubleClick() {
224  parent_->OnDoubleClick();
225}
226
227DesktopMediaSourceView* DesktopMediaListView::GetSelection() {
228  for (int i = 0; i < child_count(); ++i) {
229    DesktopMediaSourceView* source_view =
230        static_cast<DesktopMediaSourceView*>(child_at(i));
231    DCHECK_EQ(source_view->GetClassName(), kDesktopMediaSourceViewClassName);
232    if (source_view->is_selected())
233      return source_view;
234  }
235  return NULL;
236}
237
238gfx::Size DesktopMediaListView::GetPreferredSize() const {
239  int total_rows = (child_count() + kListColumns - 1) / kListColumns;
240  return gfx::Size(kTotalListWidth, GetMediaListViewHeightForRows(total_rows));
241}
242
243void DesktopMediaListView::Layout() {
244  int x = 0;
245  int y = 0;
246
247  for (int i = 0; i < child_count(); ++i) {
248    if (x + kListItemWidth > kTotalListWidth) {
249      x = 0;
250      y += kListItemHeight;
251    }
252
253    View* source_view = child_at(i);
254    source_view->SetBounds(x, y, kListItemWidth, kListItemHeight);
255
256    x += kListItemWidth;
257  }
258
259  y += kListItemHeight;
260  SetSize(gfx::Size(kTotalListWidth, y));
261}
262
263bool DesktopMediaListView::OnKeyPressed(const ui::KeyEvent& event) {
264  int position_increment = 0;
265  switch (event.key_code()) {
266    case ui::VKEY_UP:
267      position_increment = -kListColumns;
268      break;
269    case ui::VKEY_DOWN:
270      position_increment = kListColumns;
271      break;
272    case ui::VKEY_LEFT:
273      position_increment = -1;
274      break;
275    case ui::VKEY_RIGHT:
276      position_increment = 1;
277      break;
278    default:
279      return false;
280  }
281
282  if (position_increment != 0) {
283    DesktopMediaSourceView* selected = GetSelection();
284    DesktopMediaSourceView* new_selected = NULL;
285
286    if (selected) {
287      int index = GetIndexOf(selected);
288      int new_index = index + position_increment;
289      if (new_index >= child_count())
290        new_index = child_count() - 1;
291      else if (new_index < 0)
292        new_index = 0;
293      if (index != new_index) {
294        new_selected =
295            static_cast<DesktopMediaSourceView*>(child_at(new_index));
296      }
297    } else if (has_children()) {
298      new_selected = static_cast<DesktopMediaSourceView*>(child_at(0));
299    }
300
301    if (new_selected) {
302      GetFocusManager()->SetFocusedView(new_selected);
303    }
304
305    return true;
306  }
307
308  return false;
309}
310
311void DesktopMediaListView::OnSourceAdded(int index) {
312  const DesktopMediaList::Source& source = media_list_->GetSource(index);
313  DesktopMediaSourceView* source_view =
314      new DesktopMediaSourceView(this, source.id);
315  source_view->SetName(source.name);
316  source_view->SetGroup(kDesktopMediaSourceViewGroupId);
317  AddChildViewAt(source_view, index);
318
319  PreferredSizeChanged();
320
321  if (child_count() % kListColumns == 1)
322    parent_->OnMediaListRowsChanged();
323
324  std::string autoselect_source =
325      CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
326          switches::kAutoSelectDesktopCaptureSource);
327  if (!autoselect_source.empty() &&
328      base::ASCIIToUTF16(autoselect_source) == source.name) {
329    // Select, then accept and close the dialog when we're done adding sources.
330    source_view->OnFocus();
331    content::BrowserThread::PostTask(
332        content::BrowserThread::UI, FROM_HERE,
333        base::Bind(&DesktopMediaListView::AcceptSelection,
334                   weak_factory_.GetWeakPtr()));
335  }
336}
337
338void DesktopMediaListView::OnSourceRemoved(int index) {
339  DesktopMediaSourceView* view =
340      static_cast<DesktopMediaSourceView*>(child_at(index));
341  DCHECK(view);
342  DCHECK_EQ(view->GetClassName(), kDesktopMediaSourceViewClassName);
343  bool was_selected = view->is_selected();
344  RemoveChildView(view);
345  delete view;
346
347  if (was_selected)
348    OnSelectionChanged();
349
350  PreferredSizeChanged();
351
352  if (child_count() % kListColumns == 0)
353    parent_->OnMediaListRowsChanged();
354}
355
356void DesktopMediaListView::OnSourceMoved(int old_index, int new_index) {
357  DesktopMediaSourceView* view =
358      static_cast<DesktopMediaSourceView*>(child_at(old_index));
359  ReorderChildView(view, new_index);
360  PreferredSizeChanged();
361}
362
363void DesktopMediaListView::OnSourceNameChanged(int index) {
364  const DesktopMediaList::Source& source = media_list_->GetSource(index);
365  DesktopMediaSourceView* source_view =
366      static_cast<DesktopMediaSourceView*>(child_at(index));
367  source_view->SetName(source.name);
368}
369
370void DesktopMediaListView::OnSourceThumbnailChanged(int index) {
371  const DesktopMediaList::Source& source = media_list_->GetSource(index);
372  DesktopMediaSourceView* source_view =
373      static_cast<DesktopMediaSourceView*>(child_at(index));
374  source_view->SetThumbnail(source.thumbnail);
375}
376
377void DesktopMediaListView::AcceptSelection() {
378  OnSelectionChanged();
379  OnDoubleClick();
380}
381
382DesktopMediaPickerDialogView::DesktopMediaPickerDialogView(
383    content::WebContents* parent_web_contents,
384    gfx::NativeWindow context,
385    DesktopMediaPickerViews* parent,
386    const base::string16& app_name,
387    const base::string16& target_name,
388    scoped_ptr<DesktopMediaList> media_list)
389    : parent_(parent),
390      app_name_(app_name),
391      label_(new views::Label()),
392      scroll_view_(views::ScrollView::CreateScrollViewWithBorder()),
393      list_view_(new DesktopMediaListView(this, media_list.Pass())) {
394  if (app_name == target_name) {
395    label_->SetText(
396        l10n_util::GetStringFUTF16(IDS_DESKTOP_MEDIA_PICKER_TEXT, app_name));
397  } else {
398    label_->SetText(l10n_util::GetStringFUTF16(
399        IDS_DESKTOP_MEDIA_PICKER_TEXT_DELEGATED, app_name, target_name));
400  }
401  label_->SetMultiLine(true);
402  label_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
403  AddChildView(label_);
404
405  scroll_view_->SetContents(list_view_);
406  scroll_view_->ClipHeightTo(
407      GetMediaListViewHeightForRows(1), GetMediaListViewHeightForRows(2));
408  AddChildView(scroll_view_);
409
410  // If |parent_web_contents| is set and it's not a background page then the
411  // picker will be shown modal to the web contents. Otherwise the picker is
412  // shown in a separate window.
413  views::Widget* widget = NULL;
414  bool modal_dialog =
415      parent_web_contents &&
416      !parent_web_contents->GetDelegate()->IsNeverVisible(parent_web_contents);
417  if (modal_dialog) {
418    widget = CreateWebModalDialogViews(this, parent_web_contents);
419  } else {
420    widget = DialogDelegate::CreateDialogWidget(this, context, NULL);
421  }
422
423  // If the picker is not modal to the calling web contents then it is displayed
424  // in its own top-level window, so in that case it needs to be filtered out of
425  // the list of top-level windows available for capture, and to achieve that
426  // the Id is passed to DesktopMediaList.
427  DesktopMediaID::Id dialog_window_id = 0;
428  if (!modal_dialog) {
429#if defined(USE_ASH)
430    if (chrome::IsNativeWindowInAsh(widget->GetNativeWindow())) {
431      dialog_window_id =
432          DesktopMediaID::RegisterAuraWindow(widget->GetNativeWindow()).id;
433      DCHECK_NE(dialog_window_id, 0);
434    }
435#endif
436
437    if (dialog_window_id == 0) {
438      dialog_window_id = AcceleratedWidgetToDesktopMediaId(
439          widget->GetNativeWindow()->GetHost()->GetAcceleratedWidget());
440    }
441  }
442
443  list_view_->StartUpdating(dialog_window_id);
444
445  if (modal_dialog) {
446    web_modal::PopupManager* popup_manager =
447        web_modal::PopupManager::FromWebContents(parent_web_contents);
448    popup_manager->ShowModalDialog(GetWidget()->GetNativeView(),
449                                   parent_web_contents);
450  } else {
451    widget->Show();
452  }
453}
454
455DesktopMediaPickerDialogView::~DesktopMediaPickerDialogView() {}
456
457void DesktopMediaPickerDialogView::DetachParent() {
458  parent_ = NULL;
459}
460
461gfx::Size DesktopMediaPickerDialogView::GetPreferredSize() const {
462  static const size_t kDialogViewWidth = 600;
463  const gfx::Insets title_insets = views::BubbleFrameView::GetTitleInsets();
464  size_t label_height =
465      label_->GetHeightForWidth(kDialogViewWidth - title_insets.height() * 2);
466
467  return gfx::Size(kDialogViewWidth,
468                   views::kPanelVertMargin * 2 + label_height +
469                       views::kPanelVerticalSpacing +
470                       scroll_view_->GetPreferredSize().height());
471}
472
473void DesktopMediaPickerDialogView::Layout() {
474  // DialogDelegate uses the bubble style frame.
475  const gfx::Insets title_insets = views::BubbleFrameView::GetTitleInsets();
476  gfx::Rect rect = GetLocalBounds();
477
478  rect.Inset(title_insets.left(), views::kPanelVertMargin);
479
480  gfx::Rect label_rect(rect.x(), rect.y(), rect.width(),
481                       label_->GetHeightForWidth(rect.width()));
482  label_->SetBoundsRect(label_rect);
483
484  int scroll_view_top = label_rect.bottom() + views::kPanelVerticalSpacing;
485  scroll_view_->SetBounds(
486      rect.x(), scroll_view_top,
487      rect.width(), rect.height() - scroll_view_top);
488}
489
490ui::ModalType DesktopMediaPickerDialogView::GetModalType() const {
491  return ui::MODAL_TYPE_CHILD;
492}
493
494base::string16 DesktopMediaPickerDialogView::GetWindowTitle() const {
495  return l10n_util::GetStringFUTF16(IDS_DESKTOP_MEDIA_PICKER_TITLE, app_name_);
496}
497
498bool DesktopMediaPickerDialogView::IsDialogButtonEnabled(
499    ui::DialogButton button) const {
500  if (button == ui::DIALOG_BUTTON_OK)
501    return list_view_->GetSelection() != NULL;
502  return true;
503}
504
505base::string16 DesktopMediaPickerDialogView::GetDialogButtonLabel(
506    ui::DialogButton button) const {
507  return l10n_util::GetStringUTF16(button == ui::DIALOG_BUTTON_OK ?
508      IDS_DESKTOP_MEDIA_PICKER_SHARE : IDS_CANCEL);
509}
510
511bool DesktopMediaPickerDialogView::Accept() {
512  DesktopMediaSourceView* selection = list_view_->GetSelection();
513
514  // Ok button should only be enabled when a source is selected.
515  DCHECK(selection);
516
517  DesktopMediaID source;
518  if (selection)
519    source = selection->source_id();
520
521  if (parent_)
522    parent_->NotifyDialogResult(source);
523
524  // Return true to close the window.
525  return true;
526}
527
528void DesktopMediaPickerDialogView::DeleteDelegate() {
529  // If the dialog is being closed then notify the parent about it.
530  if (parent_)
531    parent_->NotifyDialogResult(DesktopMediaID());
532  delete this;
533}
534
535void DesktopMediaPickerDialogView::OnSelectionChanged() {
536  GetDialogClientView()->UpdateDialogButtons();
537}
538
539void DesktopMediaPickerDialogView::OnDoubleClick() {
540  // This will call Accept() and close the dialog.
541  GetDialogClientView()->AcceptWindow();
542}
543
544void DesktopMediaPickerDialogView::OnMediaListRowsChanged() {
545  gfx::Rect widget_bound = GetWidget()->GetWindowBoundsInScreen();
546
547  int new_height = widget_bound.height() - scroll_view_->height() +
548      scroll_view_->GetPreferredSize().height();
549
550  GetWidget()->CenterWindow(gfx::Size(widget_bound.width(), new_height));
551}
552
553DesktopMediaSourceView*
554DesktopMediaPickerDialogView::GetMediaSourceViewForTesting(int index) const {
555  if (list_view_->child_count() <= index)
556    return NULL;
557
558  return reinterpret_cast<DesktopMediaSourceView*>(list_view_->child_at(index));
559}
560
561DesktopMediaPickerViews::DesktopMediaPickerViews() : dialog_(NULL) {
562}
563
564DesktopMediaPickerViews::~DesktopMediaPickerViews() {
565  if (dialog_) {
566    dialog_->DetachParent();
567    dialog_->GetWidget()->Close();
568  }
569}
570
571void DesktopMediaPickerViews::Show(content::WebContents* web_contents,
572                                   gfx::NativeWindow context,
573                                   gfx::NativeWindow parent,
574                                   const base::string16& app_name,
575                                   const base::string16& target_name,
576                                   scoped_ptr<DesktopMediaList> media_list,
577                                   const DoneCallback& done_callback) {
578  callback_ = done_callback;
579  dialog_ = new DesktopMediaPickerDialogView(
580      web_contents, context, this, app_name, target_name, media_list.Pass());
581}
582
583void DesktopMediaPickerViews::NotifyDialogResult(DesktopMediaID source) {
584  // Once this method is called the |dialog_| will close and destroy itself.
585  dialog_->DetachParent();
586  dialog_ = NULL;
587
588  DCHECK(!callback_.is_null());
589
590  // Notify the |callback_| asynchronously because it may need to destroy
591  // DesktopMediaPicker.
592  content::BrowserThread::PostTask(
593      content::BrowserThread::UI, FROM_HERE,
594      base::Bind(callback_, source));
595  callback_.Reset();
596}
597
598// static
599scoped_ptr<DesktopMediaPicker> DesktopMediaPicker::Create() {
600  return scoped_ptr<DesktopMediaPicker>(new DesktopMediaPickerViews());
601}
602