blob: d046006b22786fb6f898f5427605512f1880db1a [file] [log] [blame]
// Copyright (c) 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/message_center/views/notifier_settings_view.h"
#include <stddef.h>
#include <set>
#include <string>
#include <utility>
#include "base/macros.h"
#include "base/strings/string16.h"
#include "base/strings/utf_string_conversions.h"
#include "skia/ext/image_operations.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/models/simple_menu_model.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/events/event_utils.h"
#include "ui/events/keycodes/keyboard_codes.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/image/image.h"
#include "ui/message_center/message_center_style.h"
#include "ui/message_center/views/message_center_view.h"
#include "ui/resources/grit/ui_resources.h"
#include "ui/strings/grit/ui_strings.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/button/checkbox.h"
#include "ui/views/controls/button/label_button_border.h"
#include "ui/views/controls/button/menu_button.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/link.h"
#include "ui/views/controls/link_listener.h"
#include "ui/views/controls/menu/menu_runner.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/controls/scrollbar/overlay_scroll_bar.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/layout/grid_layout.h"
#include "ui/views/painter.h"
#include "ui/views/widget/widget.h"
namespace message_center {
namespace settings {
// Additional views-specific parameters.
// The width of the settings pane in pixels.
const int kWidth = 360;
// The width of the learn more icon in pixels.
const int kLearnMoreSize = 12;
// The width of the click target that contains the learn more button in pixels.
const int kLearnMoreTargetWidth = 28;
// The height of the click target that contains the learn more button in pixels.
const int kLearnMoreTargetHeight = 40;
// The minimum height of the settings pane in pixels.
const int kMinimumHeight = 480;
// The horizontal margin of the title area of the settings pane in addition to
// the standard margin from settings::kHorizontalMargin.
const int kTitleMargin = 10;
} // namespace settings
namespace {
// Menu button metrics to make the text line up.
const int kMenuButtonInnateMargin = 2;
// Used to place the context menu correctly.
const int kMenuWhitespaceOffset = 2;
// The innate vertical blank space in the label for the title of the settings
// pane.
const int kInnateTitleBottomMargin = 1;
const int kInnateTitleTopMargin = 7;
// The innate top blank space in the label for the description of the settings
// pane.
const int kInnateDescriptionTopMargin = 2;
// Checkboxes have some built-in right padding blank space.
const int kInnateCheckboxRightPadding = 2;
// Spec defines the checkbox size; the innate padding throws this measurement
// off so we need to compute a slightly different area for the checkbox to
// inhabit.
const int kComputedCheckboxSize =
settings::kCheckboxSizeWithPadding - kInnateCheckboxRightPadding;
// The menubutton has innate margin, so we need to compensate for that when
// figuring the margin of the title area.
const int kComputedContentsTitleMargin = 0 - kMenuButtonInnateMargin;
// The spec doesn't include the bottom blank area of the title bar or the innate
// blank area in the description label, so we'll use this as the space between
// the title and description.
const int kComputedTitleBottomMargin = settings::kDescriptionToSwitcherSpace -
kInnateTitleBottomMargin -
kInnateDescriptionTopMargin;
// The blank space above the title needs to be adjusted by the amount of blank
// space included in the title label.
const int kComputedTitleTopMargin =
settings::kTopMargin - kInnateTitleTopMargin;
// The switcher has a lot of blank space built in so we should include that when
// spacing the title area vertically.
const int kComputedTitleElementSpacing =
settings::kDescriptionToSwitcherSpace - 6;
// A function to create a focus border.
std::unique_ptr<views::Painter> CreateFocusPainter() {
return views::Painter::CreateSolidFocusPainter(kFocusBorderColor,
gfx::Insets(1, 2, 3, 2));
}
// EntryView ------------------------------------------------------------------
// The view to guarantee the 48px height and place the contents at the
// middle. It also guarantee the left margin.
class EntryView : public views::View {
public:
explicit EntryView(views::View* contents);
~EntryView() override;
// views::View:
void Layout() override;
gfx::Size GetPreferredSize() const override;
void GetAccessibleState(ui::AXViewState* state) override;
void OnFocus() override;
bool OnKeyPressed(const ui::KeyEvent& event) override;
bool OnKeyReleased(const ui::KeyEvent& event) override;
void OnPaint(gfx::Canvas* canvas) override;
void OnBlur() override;
private:
std::unique_ptr<views::Painter> focus_painter_;
DISALLOW_COPY_AND_ASSIGN(EntryView);
};
EntryView::EntryView(views::View* contents)
: focus_painter_(CreateFocusPainter()) {
AddChildView(contents);
}
EntryView::~EntryView() {}
void EntryView::Layout() {
DCHECK_EQ(1, child_count());
views::View* content = child_at(0);
int content_width = width();
int content_height = content->GetHeightForWidth(content_width);
int y = std::max((height() - content_height) / 2, 0);
content->SetBounds(0, y, content_width, content_height);
}
gfx::Size EntryView::GetPreferredSize() const {
DCHECK_EQ(1, child_count());
gfx::Size size = child_at(0)->GetPreferredSize();
size.SetToMax(gfx::Size(settings::kWidth, settings::kEntryHeight));
return size;
}
void EntryView::GetAccessibleState(ui::AXViewState* state) {
DCHECK_EQ(1, child_count());
child_at(0)->GetAccessibleState(state);
}
void EntryView::OnFocus() {
views::View::OnFocus();
ScrollRectToVisible(GetLocalBounds());
// We render differently when focused.
SchedulePaint();
}
bool EntryView::OnKeyPressed(const ui::KeyEvent& event) {
return child_at(0)->OnKeyPressed(event);
}
bool EntryView::OnKeyReleased(const ui::KeyEvent& event) {
return child_at(0)->OnKeyReleased(event);
}
void EntryView::OnPaint(gfx::Canvas* canvas) {
View::OnPaint(canvas);
views::Painter::PaintFocusPainter(this, canvas, focus_painter_.get());
}
void EntryView::OnBlur() {
View::OnBlur();
// We render differently when focused.
SchedulePaint();
}
} // namespace
// NotifierGroupMenuModel -----------------------------------------------------
class NotifierGroupMenuModel : public ui::SimpleMenuModel,
public ui::SimpleMenuModel::Delegate {
public:
NotifierGroupMenuModel(NotifierSettingsProvider* notifier_settings_provider);
~NotifierGroupMenuModel() override;
// ui::SimpleMenuModel::Delegate:
bool IsCommandIdChecked(int command_id) const override;
bool IsCommandIdEnabled(int command_id) const override;
bool GetAcceleratorForCommandId(int command_id,
ui::Accelerator* accelerator) override;
void ExecuteCommand(int command_id, int event_flags) override;
private:
NotifierSettingsProvider* notifier_settings_provider_;
DISALLOW_COPY_AND_ASSIGN(NotifierGroupMenuModel);
};
NotifierGroupMenuModel::NotifierGroupMenuModel(
NotifierSettingsProvider* notifier_settings_provider)
: ui::SimpleMenuModel(this),
notifier_settings_provider_(notifier_settings_provider) {
if (!notifier_settings_provider_)
return;
size_t num_menu_items = notifier_settings_provider_->GetNotifierGroupCount();
for (size_t i = 0; i < num_menu_items; ++i) {
const NotifierGroup& group =
notifier_settings_provider_->GetNotifierGroupAt(i);
AddCheckItem(i, group.login_info.empty() ? group.name : group.login_info);
}
}
NotifierGroupMenuModel::~NotifierGroupMenuModel() {}
bool NotifierGroupMenuModel::IsCommandIdChecked(int command_id) const {
// If there's no provider, assume only one notifier group - the active one.
return !notifier_settings_provider_ ||
notifier_settings_provider_->IsNotifierGroupActiveAt(command_id);
}
bool NotifierGroupMenuModel::IsCommandIdEnabled(int command_id) const {
return true;
}
bool NotifierGroupMenuModel::GetAcceleratorForCommandId(
int command_id,
ui::Accelerator* accelerator) {
return false;
}
void NotifierGroupMenuModel::ExecuteCommand(int command_id, int event_flags) {
if (!notifier_settings_provider_)
return;
size_t notifier_group_index = static_cast<size_t>(command_id);
size_t num_notifier_groups =
notifier_settings_provider_->GetNotifierGroupCount();
if (notifier_group_index >= num_notifier_groups)
return;
notifier_settings_provider_->SwitchToNotifierGroup(notifier_group_index);
}
// NotifierSettingsView::NotifierButton ---------------------------------------
// We do not use views::Checkbox class directly because it doesn't support
// showing 'icon'.
NotifierSettingsView::NotifierButton::NotifierButton(
NotifierSettingsProvider* provider,
Notifier* notifier,
views::ButtonListener* listener)
: views::CustomButton(listener),
provider_(provider),
notifier_(notifier),
icon_view_(new views::ImageView()),
name_view_(new views::Label(notifier_->name)),
checkbox_(new views::Checkbox(base::string16())),
learn_more_(NULL) {
DCHECK(provider);
DCHECK(notifier);
// Since there may never be an icon (but that could change at a later time),
// we own the icon view here.
icon_view_->set_owned_by_client();
checkbox_->SetChecked(notifier_->enabled);
checkbox_->set_listener(this);
checkbox_->SetFocusBehavior(FocusBehavior::NEVER);
checkbox_->SetAccessibleName(notifier_->name);
if (ShouldHaveLearnMoreButton()) {
// Create a more-info button that will be right-aligned.
learn_more_ = new views::ImageButton(this);
learn_more_->SetFocusPainter(CreateFocusPainter());
learn_more_->set_request_focus_on_press(false);
learn_more_->SetFocusBehavior(FocusBehavior::ALWAYS);
ui::ResourceBundle& rb = ResourceBundle::GetSharedInstance();
learn_more_->SetImage(
views::Button::STATE_NORMAL,
rb.GetImageSkiaNamed(IDR_NOTIFICATION_ADVANCED_SETTINGS));
learn_more_->SetImage(
views::Button::STATE_HOVERED,
rb.GetImageSkiaNamed(IDR_NOTIFICATION_ADVANCED_SETTINGS_HOVER));
learn_more_->SetImage(
views::Button::STATE_PRESSED,
rb.GetImageSkiaNamed(IDR_NOTIFICATION_ADVANCED_SETTINGS_PRESSED));
learn_more_->SetState(views::Button::STATE_NORMAL);
int learn_more_border_width =
(settings::kLearnMoreTargetWidth - settings::kLearnMoreSize) / 2;
int learn_more_border_height =
(settings::kLearnMoreTargetHeight - settings::kLearnMoreSize) / 2;
// The image itself is quite small, this large invisible border creates a
// much bigger click target.
learn_more_->SetBorder(
views::Border::CreateEmptyBorder(learn_more_border_height,
learn_more_border_width,
learn_more_border_height,
learn_more_border_width));
learn_more_->SetImageAlignment(views::ImageButton::ALIGN_CENTER,
views::ImageButton::ALIGN_MIDDLE);
}
UpdateIconImage(notifier_->icon);
}
NotifierSettingsView::NotifierButton::~NotifierButton() {
}
void NotifierSettingsView::NotifierButton::UpdateIconImage(
const gfx::Image& icon) {
bool has_icon_view = false;
notifier_->icon = icon;
if (!icon.IsEmpty()) {
icon_view_->SetImage(icon.ToImageSkia());
icon_view_->SetImageSize(
gfx::Size(settings::kEntryIconSize, settings::kEntryIconSize));
has_icon_view = true;
}
GridChanged(ShouldHaveLearnMoreButton(), has_icon_view);
}
void NotifierSettingsView::NotifierButton::SetChecked(bool checked) {
checkbox_->SetChecked(checked);
notifier_->enabled = checked;
}
bool NotifierSettingsView::NotifierButton::checked() const {
return checkbox_->checked();
}
bool NotifierSettingsView::NotifierButton::has_learn_more() const {
return learn_more_ != NULL;
}
const Notifier& NotifierSettingsView::NotifierButton::notifier() const {
return *notifier_.get();
}
void NotifierSettingsView::NotifierButton::SendLearnMorePressedForTest() {
if (learn_more_ == NULL)
return;
gfx::Point point(110, 120);
ui::MouseEvent pressed(ui::ET_MOUSE_PRESSED, point, point,
ui::EventTimeForNow(), ui::EF_LEFT_MOUSE_BUTTON,
ui::EF_LEFT_MOUSE_BUTTON);
ButtonPressed(learn_more_, pressed);
}
void NotifierSettingsView::NotifierButton::ButtonPressed(
views::Button* button,
const ui::Event& event) {
if (button == checkbox_) {
// The checkbox state has already changed at this point, but we'll update
// the state on NotifierSettingsView::ButtonPressed() too, so here change
// back to the previous state.
checkbox_->SetChecked(!checkbox_->checked());
CustomButton::NotifyClick(event);
} else if (button == learn_more_) {
DCHECK(provider_);
provider_->OnNotifierAdvancedSettingsRequested(notifier_->notifier_id,
NULL);
}
}
void NotifierSettingsView::NotifierButton::GetAccessibleState(
ui::AXViewState* state) {
static_cast<views::View*>(checkbox_)->GetAccessibleState(state);
}
bool NotifierSettingsView::NotifierButton::ShouldHaveLearnMoreButton() const {
if (!provider_)
return false;
return provider_->NotifierHasAdvancedSettings(notifier_->notifier_id);
}
void NotifierSettingsView::NotifierButton::GridChanged(bool has_learn_more,
bool has_icon_view) {
using views::ColumnSet;
using views::GridLayout;
GridLayout* layout = new GridLayout(this);
SetLayoutManager(layout);
ColumnSet* cs = layout->AddColumnSet(0);
// Add a column for the checkbox.
cs->AddPaddingColumn(0, kInnateCheckboxRightPadding);
cs->AddColumn(GridLayout::CENTER,
GridLayout::CENTER,
0,
GridLayout::FIXED,
kComputedCheckboxSize,
0);
cs->AddPaddingColumn(0, settings::kInternalHorizontalSpacing);
if (has_icon_view) {
// Add a column for the icon.
cs->AddColumn(GridLayout::CENTER,
GridLayout::CENTER,
0,
GridLayout::FIXED,
settings::kEntryIconSize,
0);
cs->AddPaddingColumn(0, settings::kInternalHorizontalSpacing);
}
// Add a column for the name.
cs->AddColumn(
GridLayout::LEADING, GridLayout::CENTER, 0, GridLayout::USE_PREF, 0, 0);
// Add a padding column which contains expandable blank space.
cs->AddPaddingColumn(1, 0);
// Add a column for the learn more button if necessary.
if (has_learn_more) {
cs->AddPaddingColumn(0, settings::kInternalHorizontalSpacing);
cs->AddColumn(
GridLayout::CENTER, GridLayout::CENTER, 0, GridLayout::USE_PREF, 0, 0);
}
layout->StartRow(0, 0);
layout->AddView(checkbox_);
if (has_icon_view)
layout->AddView(icon_view_.get());
layout->AddView(name_view_);
if (has_learn_more)
layout->AddView(learn_more_);
Layout();
}
// NotifierSettingsView -------------------------------------------------------
NotifierSettingsView::NotifierSettingsView(NotifierSettingsProvider* provider)
: title_arrow_(NULL),
title_label_(NULL),
notifier_group_selector_(NULL),
scroller_(NULL),
provider_(provider) {
// |provider_| may be NULL in tests.
if (provider_)
provider_->AddObserver(this);
SetFocusBehavior(FocusBehavior::ALWAYS);
set_background(
views::Background::CreateSolidBackground(kMessageCenterBackgroundColor));
SetPaintToLayer(true);
title_label_ = new views::Label(
l10n_util::GetStringUTF16(IDS_MESSAGE_CENTER_SETTINGS_BUTTON_LABEL),
ui::ResourceBundle::GetSharedInstance().GetFontList(
ui::ResourceBundle::MediumFont));
title_label_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
title_label_->SetMultiLine(true);
title_label_->SetBorder(
views::Border::CreateEmptyBorder(kComputedTitleTopMargin,
settings::kTitleMargin,
kComputedTitleBottomMargin,
settings::kTitleMargin));
AddChildView(title_label_);
scroller_ = new views::ScrollView();
scroller_->SetVerticalScrollBar(new views::OverlayScrollBar(false));
AddChildView(scroller_);
std::vector<Notifier*> notifiers;
if (provider_)
provider_->GetNotifierList(&notifiers);
UpdateContentsView(notifiers);
}
NotifierSettingsView::~NotifierSettingsView() {
// |provider_| may be NULL in tests.
if (provider_)
provider_->RemoveObserver(this);
}
bool NotifierSettingsView::IsScrollable() {
return scroller_->height() < scroller_->contents()->height();
}
void NotifierSettingsView::UpdateIconImage(const NotifierId& notifier_id,
const gfx::Image& icon) {
for (std::set<NotifierButton*>::iterator iter = buttons_.begin();
iter != buttons_.end();
++iter) {
if ((*iter)->notifier().notifier_id == notifier_id) {
(*iter)->UpdateIconImage(icon);
return;
}
}
}
void NotifierSettingsView::NotifierGroupChanged() {
std::vector<Notifier*> notifiers;
if (provider_)
provider_->GetNotifierList(&notifiers);
UpdateContentsView(notifiers);
}
void NotifierSettingsView::NotifierEnabledChanged(const NotifierId& notifier_id,
bool enabled) {}
void NotifierSettingsView::UpdateContentsView(
const std::vector<Notifier*>& notifiers) {
buttons_.clear();
views::View* contents_view = new views::View();
contents_view->SetLayoutManager(new views::BoxLayout(
views::BoxLayout::kVertical, settings::kHorizontalMargin, 0, 0));
views::View* contents_title_view = new views::View();
contents_title_view->SetLayoutManager(
new views::BoxLayout(views::BoxLayout::kVertical,
kComputedContentsTitleMargin,
0,
kComputedTitleElementSpacing));
bool need_account_switcher =
provider_ && provider_->GetNotifierGroupCount() > 1;
int top_label_resource_id =
need_account_switcher ? IDS_MESSAGE_CENTER_SETTINGS_DESCRIPTION_MULTIUSER
: IDS_MESSAGE_CENTER_SETTINGS_DIALOG_DESCRIPTION;
views::Label* top_label =
new views::Label(l10n_util::GetStringUTF16(top_label_resource_id));
top_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
top_label->SetMultiLine(true);
top_label->SetBorder(views::Border::CreateEmptyBorder(
0,
settings::kTitleMargin + kMenuButtonInnateMargin,
0,
settings::kTitleMargin + kMenuButtonInnateMargin));
contents_title_view->AddChildView(top_label);
if (need_account_switcher) {
const NotifierGroup& active_group = provider_->GetActiveNotifierGroup();
base::string16 notifier_group_text = active_group.login_info.empty() ?
active_group.name : active_group.login_info;
notifier_group_selector_ =
new views::MenuButton(notifier_group_text, this, true);
notifier_group_selector_->SetBorder(std::unique_ptr<views::Border>(
new views::LabelButtonAssetBorder(views::Button::STYLE_BUTTON)));
notifier_group_selector_->SetFocusPainter(nullptr);
notifier_group_selector_->set_animate_on_state_change(false);
notifier_group_selector_->SetFocusBehavior(FocusBehavior::ALWAYS);
contents_title_view->AddChildView(notifier_group_selector_);
}
contents_view->AddChildView(contents_title_view);
size_t notifier_count = notifiers.size();
for (size_t i = 0; i < notifier_count; ++i) {
NotifierButton* button = new NotifierButton(provider_, notifiers[i], this);
EntryView* entry = new EntryView(button);
// This code emulates separators using borders. We will create an invisible
// border on the last notifier, as the spec leaves a space for it.
std::unique_ptr<views::Border> entry_border;
if (i == notifier_count - 1) {
entry_border = views::Border::CreateEmptyBorder(
0, 0, settings::kEntrySeparatorHeight, 0);
} else {
entry_border =
views::Border::CreateSolidSidedBorder(0,
0,
settings::kEntrySeparatorHeight,
0,
settings::kEntrySeparatorColor);
}
entry->SetBorder(std::move(entry_border));
entry->SetFocusBehavior(FocusBehavior::ALWAYS);
contents_view->AddChildView(entry);
buttons_.insert(button);
}
scroller_->SetContents(contents_view);
contents_view->SetBoundsRect(gfx::Rect(contents_view->GetPreferredSize()));
InvalidateLayout();
}
void NotifierSettingsView::Layout() {
int title_height = title_label_->GetHeightForWidth(width());
title_label_->SetBounds(settings::kTitleMargin,
0,
width() - settings::kTitleMargin * 2,
title_height);
views::View* contents_view = scroller_->contents();
int content_width = width();
int content_height = contents_view->GetHeightForWidth(content_width);
if (title_height + content_height > height()) {
content_width -= scroller_->GetScrollBarWidth();
content_height = contents_view->GetHeightForWidth(content_width);
}
contents_view->SetBounds(0, 0, content_width, content_height);
scroller_->SetBounds(0, title_height, width(), height() - title_height);
}
gfx::Size NotifierSettingsView::GetMinimumSize() const {
gfx::Size size(settings::kWidth, settings::kMinimumHeight);
int total_height = title_label_->GetPreferredSize().height() +
scroller_->contents()->GetPreferredSize().height();
if (total_height > settings::kMinimumHeight)
size.Enlarge(scroller_->GetScrollBarWidth(), 0);
return size;
}
gfx::Size NotifierSettingsView::GetPreferredSize() const {
gfx::Size preferred_size;
gfx::Size title_size = title_label_->GetPreferredSize();
gfx::Size content_size = scroller_->contents()->GetPreferredSize();
return gfx::Size(std::max(title_size.width(), content_size.width()),
title_size.height() + content_size.height());
}
bool NotifierSettingsView::OnKeyPressed(const ui::KeyEvent& event) {
if (event.key_code() == ui::VKEY_ESCAPE) {
GetWidget()->Close();
return true;
}
return scroller_->OnKeyPressed(event);
}
bool NotifierSettingsView::OnMouseWheel(const ui::MouseWheelEvent& event) {
return scroller_->OnMouseWheel(event);
}
void NotifierSettingsView::ButtonPressed(views::Button* sender,
const ui::Event& event) {
if (sender == title_arrow_) {
MessageCenterView* center_view = static_cast<MessageCenterView*>(parent());
center_view->SetSettingsVisible(!center_view->settings_visible());
return;
}
std::set<NotifierButton*>::iterator iter =
buttons_.find(static_cast<NotifierButton*>(sender));
if (iter == buttons_.end())
return;
(*iter)->SetChecked(!(*iter)->checked());
if (provider_)
provider_->SetNotifierEnabled((*iter)->notifier(), (*iter)->checked());
}
void NotifierSettingsView::OnMenuButtonClicked(views::MenuButton* source,
const gfx::Point& point,
const ui::Event* event) {
notifier_group_menu_model_.reset(new NotifierGroupMenuModel(provider_));
notifier_group_menu_runner_.reset(new views::MenuRunner(
notifier_group_menu_model_.get(), views::MenuRunner::CONTEXT_MENU));
gfx::Rect menu_anchor = source->GetBoundsInScreen();
menu_anchor.Inset(
gfx::Insets(0, kMenuWhitespaceOffset, 0, kMenuWhitespaceOffset));
if (views::MenuRunner::MENU_DELETED ==
notifier_group_menu_runner_->RunMenuAt(GetWidget(),
notifier_group_selector_,
menu_anchor,
views::MENU_ANCHOR_BUBBLE_ABOVE,
ui::MENU_SOURCE_MOUSE))
return;
MessageCenterView* center_view = static_cast<MessageCenterView*>(parent());
center_view->OnSettingsChanged();
}
} // namespace message_center