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