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