1 // Copyright (c) 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.
5 #include "ui/message_center/views/notifier_settings_view.h"
10 #include "base/strings/string16.h"
11 #include "base/strings/utf_string_conversions.h"
12 #include "skia/ext/image_operations.h"
13 #include "third_party/skia/include/core/SkColor.h"
14 #include "ui/base/l10n/l10n_util.h"
15 #include "ui/base/models/simple_menu_model.h"
16 #include "ui/base/resource/resource_bundle.h"
17 #include "ui/events/keycodes/keyboard_codes.h"
18 #include "ui/gfx/canvas.h"
19 #include "ui/gfx/image/image.h"
20 #include "ui/gfx/size.h"
21 #include "ui/message_center/message_center_style.h"
22 #include "ui/message_center/views/message_center_view.h"
23 #include "ui/resources/grit/ui_resources.h"
24 #include "ui/strings/grit/ui_strings.h"
25 #include "ui/views/background.h"
26 #include "ui/views/border.h"
27 #include "ui/views/controls/button/checkbox.h"
28 #include "ui/views/controls/button/label_button_border.h"
29 #include "ui/views/controls/button/menu_button.h"
30 #include "ui/views/controls/image_view.h"
31 #include "ui/views/controls/label.h"
32 #include "ui/views/controls/link.h"
33 #include "ui/views/controls/link_listener.h"
34 #include "ui/views/controls/menu/menu_runner.h"
35 #include "ui/views/controls/scroll_view.h"
36 #include "ui/views/controls/scrollbar/overlay_scroll_bar.h"
37 #include "ui/views/layout/box_layout.h"
38 #include "ui/views/layout/fill_layout.h"
39 #include "ui/views/layout/grid_layout.h"
40 #include "ui/views/painter.h"
41 #include "ui/views/widget/widget.h"
43 namespace message_center {
46 // Additional views-specific parameters.
48 // The width of the settings pane in pixels.
49 const int kWidth = 360;
51 // The width of the learn more icon in pixels.
52 const int kLearnMoreSize = 12;
54 // The width of the click target that contains the learn more button in pixels.
55 const int kLearnMoreTargetWidth = 28;
57 // The height of the click target that contains the learn more button in pixels.
58 const int kLearnMoreTargetHeight = 40;
60 // The minimum height of the settings pane in pixels.
61 const int kMinimumHeight = 480;
63 // The horizontal margin of the title area of the settings pane in addition to
64 // the standard margin from settings::kHorizontalMargin.
65 const int kTitleMargin = 10;
67 } // namespace settings
71 // Menu button metrics to make the text line up.
72 const int kMenuButtonInnateMargin = 2;
74 // Used to place the context menu correctly.
75 const int kMenuWhitespaceOffset = 2;
77 // The innate vertical blank space in the label for the title of the settings
79 const int kInnateTitleBottomMargin = 1;
80 const int kInnateTitleTopMargin = 7;
82 // The innate top blank space in the label for the description of the settings
84 const int kInnateDescriptionTopMargin = 2;
86 // Checkboxes have some built-in right padding blank space.
87 const int kInnateCheckboxRightPadding = 2;
89 // Spec defines the checkbox size; the innate padding throws this measurement
90 // off so we need to compute a slightly different area for the checkbox to
92 const int kComputedCheckboxSize =
93 settings::kCheckboxSizeWithPadding - kInnateCheckboxRightPadding;
95 // The menubutton has innate margin, so we need to compensate for that when
96 // figuring the margin of the title area.
97 const int kComputedContentsTitleMargin = 0 - kMenuButtonInnateMargin;
99 // The spec doesn't include the bottom blank area of the title bar or the innate
100 // blank area in the description label, so we'll use this as the space between
101 // the title and description.
102 const int kComputedTitleBottomMargin = settings::kDescriptionToSwitcherSpace -
103 kInnateTitleBottomMargin -
104 kInnateDescriptionTopMargin;
106 // The blank space above the title needs to be adjusted by the amount of blank
107 // space included in the title label.
108 const int kComputedTitleTopMargin =
109 settings::kTopMargin - kInnateTitleTopMargin;
111 // The switcher has a lot of blank space built in so we should include that when
112 // spacing the title area vertically.
113 const int kComputedTitleElementSpacing =
114 settings::kDescriptionToSwitcherSpace - 6;
116 // A function to create a focus border.
117 scoped_ptr<views::Painter> CreateFocusPainter() {
118 return views::Painter::CreateSolidFocusPainter(kFocusBorderColor,
119 gfx::Insets(1, 2, 3, 2));
122 // EntryView ------------------------------------------------------------------
124 // The view to guarantee the 48px height and place the contents at the
125 // middle. It also guarantee the left margin.
126 class EntryView : public views::View {
128 explicit EntryView(views::View* contents);
129 ~EntryView() override;
132 void Layout() override;
133 gfx::Size GetPreferredSize() const override;
134 void GetAccessibleState(ui::AXViewState* state) override;
135 void OnFocus() override;
136 bool OnKeyPressed(const ui::KeyEvent& event) override;
137 bool OnKeyReleased(const ui::KeyEvent& event) override;
138 void OnPaint(gfx::Canvas* canvas) override;
139 void OnBlur() override;
142 scoped_ptr<views::Painter> focus_painter_;
144 DISALLOW_COPY_AND_ASSIGN(EntryView);
147 EntryView::EntryView(views::View* contents)
148 : focus_painter_(CreateFocusPainter()) {
149 AddChildView(contents);
152 EntryView::~EntryView() {}
154 void EntryView::Layout() {
155 DCHECK_EQ(1, child_count());
156 views::View* content = child_at(0);
157 int content_width = width();
158 int content_height = content->GetHeightForWidth(content_width);
159 int y = std::max((height() - content_height) / 2, 0);
160 content->SetBounds(0, y, content_width, content_height);
163 gfx::Size EntryView::GetPreferredSize() const {
164 DCHECK_EQ(1, child_count());
165 gfx::Size size = child_at(0)->GetPreferredSize();
166 size.SetToMax(gfx::Size(settings::kWidth, settings::kEntryHeight));
170 void EntryView::GetAccessibleState(ui::AXViewState* state) {
171 DCHECK_EQ(1, child_count());
172 child_at(0)->GetAccessibleState(state);
175 void EntryView::OnFocus() {
176 views::View::OnFocus();
177 ScrollRectToVisible(GetLocalBounds());
178 // We render differently when focused.
182 bool EntryView::OnKeyPressed(const ui::KeyEvent& event) {
183 return child_at(0)->OnKeyPressed(event);
186 bool EntryView::OnKeyReleased(const ui::KeyEvent& event) {
187 return child_at(0)->OnKeyReleased(event);
190 void EntryView::OnPaint(gfx::Canvas* canvas) {
191 View::OnPaint(canvas);
192 views::Painter::PaintFocusPainter(this, canvas, focus_painter_.get());
195 void EntryView::OnBlur() {
197 // We render differently when focused.
204 // NotifierGroupMenuModel -----------------------------------------------------
206 class NotifierGroupMenuModel : public ui::SimpleMenuModel,
207 public ui::SimpleMenuModel::Delegate {
209 NotifierGroupMenuModel(NotifierSettingsProvider* notifier_settings_provider);
210 ~NotifierGroupMenuModel() override;
212 // ui::SimpleMenuModel::Delegate:
213 bool IsCommandIdChecked(int command_id) const override;
214 bool IsCommandIdEnabled(int command_id) const override;
215 bool GetAcceleratorForCommandId(int command_id,
216 ui::Accelerator* accelerator) override;
217 void ExecuteCommand(int command_id, int event_flags) override;
220 NotifierSettingsProvider* notifier_settings_provider_;
222 DISALLOW_COPY_AND_ASSIGN(NotifierGroupMenuModel);
225 NotifierGroupMenuModel::NotifierGroupMenuModel(
226 NotifierSettingsProvider* notifier_settings_provider)
227 : ui::SimpleMenuModel(this),
228 notifier_settings_provider_(notifier_settings_provider) {
229 if (!notifier_settings_provider_)
232 size_t num_menu_items = notifier_settings_provider_->GetNotifierGroupCount();
233 for (size_t i = 0; i < num_menu_items; ++i) {
234 const NotifierGroup& group =
235 notifier_settings_provider_->GetNotifierGroupAt(i);
237 AddCheckItem(i, group.login_info.empty() ? group.name : group.login_info);
241 NotifierGroupMenuModel::~NotifierGroupMenuModel() {}
243 bool NotifierGroupMenuModel::IsCommandIdChecked(int command_id) const {
244 // If there's no provider, assume only one notifier group - the active one.
245 return !notifier_settings_provider_ ||
246 notifier_settings_provider_->IsNotifierGroupActiveAt(command_id);
249 bool NotifierGroupMenuModel::IsCommandIdEnabled(int command_id) const {
253 bool NotifierGroupMenuModel::GetAcceleratorForCommandId(
255 ui::Accelerator* accelerator) {
259 void NotifierGroupMenuModel::ExecuteCommand(int command_id, int event_flags) {
260 if (!notifier_settings_provider_)
263 size_t notifier_group_index = static_cast<size_t>(command_id);
264 size_t num_notifier_groups =
265 notifier_settings_provider_->GetNotifierGroupCount();
266 if (notifier_group_index >= num_notifier_groups)
269 notifier_settings_provider_->SwitchToNotifierGroup(notifier_group_index);
273 // NotifierSettingsView::NotifierButton ---------------------------------------
275 // We do not use views::Checkbox class directly because it doesn't support
277 NotifierSettingsView::NotifierButton::NotifierButton(
278 NotifierSettingsProvider* provider,
280 views::ButtonListener* listener)
281 : views::CustomButton(listener),
284 icon_view_(new views::ImageView()),
285 name_view_(new views::Label(notifier_->name)),
286 checkbox_(new views::Checkbox(base::string16())),
291 // Since there may never be an icon (but that could change at a later time),
292 // we own the icon view here.
293 icon_view_->set_owned_by_client();
295 checkbox_->SetChecked(notifier_->enabled);
296 checkbox_->set_listener(this);
297 checkbox_->SetFocusable(false);
298 checkbox_->SetAccessibleName(notifier_->name);
300 if (ShouldHaveLearnMoreButton()) {
301 // Create a more-info button that will be right-aligned.
302 learn_more_ = new views::ImageButton(this);
303 learn_more_->SetFocusPainter(CreateFocusPainter());
304 learn_more_->set_request_focus_on_press(false);
305 learn_more_->SetFocusable(true);
307 ui::ResourceBundle& rb = ResourceBundle::GetSharedInstance();
308 learn_more_->SetImage(
309 views::Button::STATE_NORMAL,
310 rb.GetImageSkiaNamed(IDR_NOTIFICATION_ADVANCED_SETTINGS));
311 learn_more_->SetImage(
312 views::Button::STATE_HOVERED,
313 rb.GetImageSkiaNamed(IDR_NOTIFICATION_ADVANCED_SETTINGS_HOVER));
314 learn_more_->SetImage(
315 views::Button::STATE_PRESSED,
316 rb.GetImageSkiaNamed(IDR_NOTIFICATION_ADVANCED_SETTINGS_PRESSED));
317 learn_more_->SetState(views::Button::STATE_NORMAL);
318 int learn_more_border_width =
319 (settings::kLearnMoreTargetWidth - settings::kLearnMoreSize) / 2;
320 int learn_more_border_height =
321 (settings::kLearnMoreTargetHeight - settings::kLearnMoreSize) / 2;
322 // The image itself is quite small, this large invisible border creates a
323 // much bigger click target.
324 learn_more_->SetBorder(
325 views::Border::CreateEmptyBorder(learn_more_border_height,
326 learn_more_border_width,
327 learn_more_border_height,
328 learn_more_border_width));
329 learn_more_->SetImageAlignment(views::ImageButton::ALIGN_CENTER,
330 views::ImageButton::ALIGN_MIDDLE);
333 UpdateIconImage(notifier_->icon);
336 NotifierSettingsView::NotifierButton::~NotifierButton() {
339 void NotifierSettingsView::NotifierButton::UpdateIconImage(
340 const gfx::Image& icon) {
341 bool has_icon_view = false;
343 notifier_->icon = icon;
344 if (!icon.IsEmpty()) {
345 icon_view_->SetImage(icon.ToImageSkia());
346 icon_view_->SetImageSize(
347 gfx::Size(settings::kEntryIconSize, settings::kEntryIconSize));
348 has_icon_view = true;
350 GridChanged(ShouldHaveLearnMoreButton(), has_icon_view);
353 void NotifierSettingsView::NotifierButton::SetChecked(bool checked) {
354 checkbox_->SetChecked(checked);
355 notifier_->enabled = checked;
358 bool NotifierSettingsView::NotifierButton::checked() const {
359 return checkbox_->checked();
362 bool NotifierSettingsView::NotifierButton::has_learn_more() const {
363 return learn_more_ != NULL;
366 const Notifier& NotifierSettingsView::NotifierButton::notifier() const {
367 return *notifier_.get();
370 void NotifierSettingsView::NotifierButton::SendLearnMorePressedForTest() {
371 if (learn_more_ == NULL)
373 gfx::Point point(110, 120);
374 ui::MouseEvent pressed(
375 ui::ET_MOUSE_PRESSED, point, point, ui::EF_LEFT_MOUSE_BUTTON,
376 ui::EF_LEFT_MOUSE_BUTTON);
377 ButtonPressed(learn_more_, pressed);
380 void NotifierSettingsView::NotifierButton::ButtonPressed(
381 views::Button* button,
382 const ui::Event& event) {
383 if (button == checkbox_) {
384 // The checkbox state has already changed at this point, but we'll update
385 // the state on NotifierSettingsView::ButtonPressed() too, so here change
386 // back to the previous state.
387 checkbox_->SetChecked(!checkbox_->checked());
388 CustomButton::NotifyClick(event);
389 } else if (button == learn_more_) {
391 provider_->OnNotifierAdvancedSettingsRequested(notifier_->notifier_id,
396 void NotifierSettingsView::NotifierButton::GetAccessibleState(
397 ui::AXViewState* state) {
398 static_cast<views::View*>(checkbox_)->GetAccessibleState(state);
401 bool NotifierSettingsView::NotifierButton::ShouldHaveLearnMoreButton() const {
405 return provider_->NotifierHasAdvancedSettings(notifier_->notifier_id);
408 void NotifierSettingsView::NotifierButton::GridChanged(bool has_learn_more,
409 bool has_icon_view) {
410 using views::ColumnSet;
411 using views::GridLayout;
413 GridLayout* layout = new GridLayout(this);
414 SetLayoutManager(layout);
415 ColumnSet* cs = layout->AddColumnSet(0);
416 // Add a column for the checkbox.
417 cs->AddPaddingColumn(0, kInnateCheckboxRightPadding);
418 cs->AddColumn(GridLayout::CENTER,
422 kComputedCheckboxSize,
424 cs->AddPaddingColumn(0, settings::kInternalHorizontalSpacing);
427 // Add a column for the icon.
428 cs->AddColumn(GridLayout::CENTER,
432 settings::kEntryIconSize,
434 cs->AddPaddingColumn(0, settings::kInternalHorizontalSpacing);
437 // Add a column for the name.
439 GridLayout::LEADING, GridLayout::CENTER, 0, GridLayout::USE_PREF, 0, 0);
441 // Add a padding column which contains expandable blank space.
442 cs->AddPaddingColumn(1, 0);
444 // Add a column for the learn more button if necessary.
445 if (has_learn_more) {
446 cs->AddPaddingColumn(0, settings::kInternalHorizontalSpacing);
448 GridLayout::CENTER, GridLayout::CENTER, 0, GridLayout::USE_PREF, 0, 0);
451 layout->StartRow(0, 0);
452 layout->AddView(checkbox_);
454 layout->AddView(icon_view_.get());
455 layout->AddView(name_view_);
457 layout->AddView(learn_more_);
463 // NotifierSettingsView -------------------------------------------------------
465 NotifierSettingsView::NotifierSettingsView(NotifierSettingsProvider* provider)
466 : title_arrow_(NULL),
468 notifier_group_selector_(NULL),
470 provider_(provider) {
471 // |provider_| may be NULL in tests.
473 provider_->AddObserver(this);
477 views::Background::CreateSolidBackground(kMessageCenterBackgroundColor));
478 SetPaintToLayer(true);
480 title_label_ = new views::Label(
481 l10n_util::GetStringUTF16(IDS_MESSAGE_CENTER_SETTINGS_BUTTON_LABEL),
482 ui::ResourceBundle::GetSharedInstance().GetFontList(
483 ui::ResourceBundle::MediumFont));
484 title_label_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
485 title_label_->SetMultiLine(true);
486 title_label_->SetBorder(
487 views::Border::CreateEmptyBorder(kComputedTitleTopMargin,
488 settings::kTitleMargin,
489 kComputedTitleBottomMargin,
490 settings::kTitleMargin));
492 AddChildView(title_label_);
494 scroller_ = new views::ScrollView();
495 scroller_->SetVerticalScrollBar(new views::OverlayScrollBar(false));
496 AddChildView(scroller_);
498 std::vector<Notifier*> notifiers;
500 provider_->GetNotifierList(¬ifiers);
502 UpdateContentsView(notifiers);
505 NotifierSettingsView::~NotifierSettingsView() {
506 // |provider_| may be NULL in tests.
508 provider_->RemoveObserver(this);
511 bool NotifierSettingsView::IsScrollable() {
512 return scroller_->height() < scroller_->contents()->height();
515 void NotifierSettingsView::UpdateIconImage(const NotifierId& notifier_id,
516 const gfx::Image& icon) {
517 for (std::set<NotifierButton*>::iterator iter = buttons_.begin();
518 iter != buttons_.end();
520 if ((*iter)->notifier().notifier_id == notifier_id) {
521 (*iter)->UpdateIconImage(icon);
527 void NotifierSettingsView::NotifierGroupChanged() {
528 std::vector<Notifier*> notifiers;
530 provider_->GetNotifierList(¬ifiers);
532 UpdateContentsView(notifiers);
535 void NotifierSettingsView::NotifierEnabledChanged(const NotifierId& notifier_id,
538 void NotifierSettingsView::UpdateContentsView(
539 const std::vector<Notifier*>& notifiers) {
542 views::View* contents_view = new views::View();
543 contents_view->SetLayoutManager(new views::BoxLayout(
544 views::BoxLayout::kVertical, settings::kHorizontalMargin, 0, 0));
546 views::View* contents_title_view = new views::View();
547 contents_title_view->SetLayoutManager(
548 new views::BoxLayout(views::BoxLayout::kVertical,
549 kComputedContentsTitleMargin,
551 kComputedTitleElementSpacing));
553 bool need_account_switcher =
554 provider_ && provider_->GetNotifierGroupCount() > 1;
555 int top_label_resource_id =
556 need_account_switcher ? IDS_MESSAGE_CENTER_SETTINGS_DESCRIPTION_MULTIUSER
557 : IDS_MESSAGE_CENTER_SETTINGS_DIALOG_DESCRIPTION;
559 views::Label* top_label =
560 new views::Label(l10n_util::GetStringUTF16(top_label_resource_id));
562 top_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
563 top_label->SetMultiLine(true);
564 top_label->SetBorder(views::Border::CreateEmptyBorder(
566 settings::kTitleMargin + kMenuButtonInnateMargin,
568 settings::kTitleMargin + kMenuButtonInnateMargin));
569 contents_title_view->AddChildView(top_label);
571 if (need_account_switcher) {
572 const NotifierGroup& active_group = provider_->GetActiveNotifierGroup();
573 base::string16 notifier_group_text = active_group.login_info.empty() ?
574 active_group.name : active_group.login_info;
575 notifier_group_selector_ =
576 new views::MenuButton(NULL, notifier_group_text, this, true);
577 notifier_group_selector_->SetBorder(scoped_ptr<views::Border>(
578 new views::LabelButtonBorder(views::Button::STYLE_BUTTON)).Pass());
579 notifier_group_selector_->SetFocusPainter(scoped_ptr<views::Painter>());
580 notifier_group_selector_->set_animate_on_state_change(false);
581 notifier_group_selector_->SetFocusable(true);
582 contents_title_view->AddChildView(notifier_group_selector_);
585 contents_view->AddChildView(contents_title_view);
587 size_t notifier_count = notifiers.size();
588 for (size_t i = 0; i < notifier_count; ++i) {
589 NotifierButton* button = new NotifierButton(provider_, notifiers[i], this);
590 EntryView* entry = new EntryView(button);
592 // This code emulates separators using borders. We will create an invisible
593 // border on the last notifier, as the spec leaves a space for it.
594 scoped_ptr<views::Border> entry_border;
595 if (i == notifier_count - 1) {
596 entry_border = views::Border::CreateEmptyBorder(
597 0, 0, settings::kEntrySeparatorHeight, 0);
600 views::Border::CreateSolidSidedBorder(0,
602 settings::kEntrySeparatorHeight,
604 settings::kEntrySeparatorColor);
606 entry->SetBorder(entry_border.Pass());
607 entry->SetFocusable(true);
608 contents_view->AddChildView(entry);
609 buttons_.insert(button);
612 scroller_->SetContents(contents_view);
614 contents_view->SetBoundsRect(gfx::Rect(contents_view->GetPreferredSize()));
618 void NotifierSettingsView::Layout() {
619 int title_height = title_label_->GetHeightForWidth(width());
620 title_label_->SetBounds(settings::kTitleMargin,
622 width() - settings::kTitleMargin * 2,
625 views::View* contents_view = scroller_->contents();
626 int content_width = width();
627 int content_height = contents_view->GetHeightForWidth(content_width);
628 if (title_height + content_height > height()) {
629 content_width -= scroller_->GetScrollBarWidth();
630 content_height = contents_view->GetHeightForWidth(content_width);
632 contents_view->SetBounds(0, 0, content_width, content_height);
633 scroller_->SetBounds(0, title_height, width(), height() - title_height);
636 gfx::Size NotifierSettingsView::GetMinimumSize() const {
637 gfx::Size size(settings::kWidth, settings::kMinimumHeight);
638 int total_height = title_label_->GetPreferredSize().height() +
639 scroller_->contents()->GetPreferredSize().height();
640 if (total_height > settings::kMinimumHeight)
641 size.Enlarge(scroller_->GetScrollBarWidth(), 0);
645 gfx::Size NotifierSettingsView::GetPreferredSize() const {
646 gfx::Size preferred_size;
647 gfx::Size title_size = title_label_->GetPreferredSize();
648 gfx::Size content_size = scroller_->contents()->GetPreferredSize();
649 return gfx::Size(std::max(title_size.width(), content_size.width()),
650 title_size.height() + content_size.height());
653 bool NotifierSettingsView::OnKeyPressed(const ui::KeyEvent& event) {
654 if (event.key_code() == ui::VKEY_ESCAPE) {
655 GetWidget()->Close();
659 return scroller_->OnKeyPressed(event);
662 bool NotifierSettingsView::OnMouseWheel(const ui::MouseWheelEvent& event) {
663 return scroller_->OnMouseWheel(event);
666 void NotifierSettingsView::ButtonPressed(views::Button* sender,
667 const ui::Event& event) {
668 if (sender == title_arrow_) {
669 MessageCenterView* center_view = static_cast<MessageCenterView*>(parent());
670 center_view->SetSettingsVisible(!center_view->settings_visible());
674 std::set<NotifierButton*>::iterator iter =
675 buttons_.find(static_cast<NotifierButton*>(sender));
677 if (iter == buttons_.end())
680 (*iter)->SetChecked(!(*iter)->checked());
682 provider_->SetNotifierEnabled((*iter)->notifier(), (*iter)->checked());
685 void NotifierSettingsView::OnMenuButtonClicked(views::View* source,
686 const gfx::Point& point) {
687 notifier_group_menu_model_.reset(new NotifierGroupMenuModel(provider_));
688 notifier_group_menu_runner_.reset(new views::MenuRunner(
689 notifier_group_menu_model_.get(), views::MenuRunner::CONTEXT_MENU));
690 gfx::Rect menu_anchor = source->GetBoundsInScreen();
692 gfx::Insets(0, kMenuWhitespaceOffset, 0, kMenuWhitespaceOffset));
693 if (views::MenuRunner::MENU_DELETED ==
694 notifier_group_menu_runner_->RunMenuAt(GetWidget(),
695 notifier_group_selector_,
697 views::MENU_ANCHOR_BUBBLE_ABOVE,
698 ui::MENU_SOURCE_MOUSE))
700 MessageCenterView* center_view = static_cast<MessageCenterView*>(parent());
701 center_view->OnSettingsChanged();
704 } // namespace message_center