blob: 98f730e7496807c43028f24648c2468bcbd37f20 [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 "ash/system/message_center/notifier_settings_view.h"
#include <stddef.h>
#include <set>
#include <string>
#include <utility>
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/message_center/message_center_controller.h"
#include "ash/system/message_center/message_center_style.h"
#include "ash/system/tray/tray_constants.h"
#include "ash/system/tray/tray_popup_utils.h"
#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/accessibility/ax_node_data.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/compositor/paint_recorder.h"
#include "ui/events/event_utils.h"
#include "ui/events/keycodes/keyboard_codes.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/skia_paint_util.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/public/cpp/message_center_constants.h"
#include "ui/message_center/vector_icons.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/toggle_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/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 ash {
using message_center::MessageCenter;
using mojom::NotifierUiData;
using message_center::NotifierId;
namespace {
const int kNotifierButtonWrapperHeight = 48;
const int kHorizontalMargin = 12;
const int kEntryIconSize = 20;
const int kInternalHorizontalSpacing = 16;
const int kSmallerInternalHorizontalSpacing = 12;
const int kCheckboxSizeWithPadding = 28;
// The width of the settings pane in pixels.
const int kWidth = 360;
// The minimum height of the settings pane in pixels.
const int kMinimumHeight = 480;
// 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.
constexpr int kComputedCheckboxSize =
kCheckboxSizeWithPadding - kInnateCheckboxRightPadding;
// TODO(tetsui): Give more general names and remove kEntryHeight, etc.
constexpr gfx::Insets kTopLabelPadding(16, 18, 15, 18);
const int kQuietModeViewSpacing = 18;
constexpr gfx::Insets kHeaderViewPadding(4, 0);
constexpr gfx::Insets kQuietModeViewPadding(0, 18, 0, 0);
constexpr gfx::Insets kQuietModeLabelPadding(16, 0, 15, 0);
constexpr gfx::Insets kQuietModeTogglePadding(0, 14);
constexpr SkColor kTopBorderColor = SkColorSetA(SK_ColorBLACK, 0x1F);
constexpr SkColor kDisabledNotifierFilterColor =
SkColorSetA(SK_ColorWHITE, 0xB8);
const int kLabelFontSizeDelta = 1;
// NotifierButtonWrapperView ---------------------------------------------------
// A wrapper view of NotifierButton to guarantee the fixed height
// |kNotifierButtonWrapperHeight|. The button is placed in the middle of
// the wrapper view by giving padding to the top and the bottom.
// The view is focusable and provides focus painter. When the button is disabled
// (NotifierUiData.enforced), it also applies filter to make the color of the
// button dim.
class NotifierButtonWrapperView : public views::View {
public:
explicit NotifierButtonWrapperView(views::View* contents);
~NotifierButtonWrapperView() override;
// views::View:
void Layout() override;
gfx::Size CalculatePreferredSize() const override;
void GetAccessibleNodeData(ui::AXNodeData* node_data) 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:
// Initialize |disabled_filter_|. Should be called once.
void CreateDisabledFilter();
std::unique_ptr<views::Painter> focus_painter_;
// NotifierButton to wrap.
views::View* contents_;
// A view to add semi-transparent filter on top of |contents_|.
// It is only visible when NotifierButton is disabled (e.g. the setting is
// enforced by administrator.) The color of the NotifierButton would be dim
// and users notice they can't change the setting.
views::View* disabled_filter_ = nullptr;
DISALLOW_COPY_AND_ASSIGN(NotifierButtonWrapperView);
};
NotifierButtonWrapperView::NotifierButtonWrapperView(views::View* contents)
: focus_painter_(TrayPopupUtils::CreateFocusPainter()),
contents_(contents) {
AddChildView(contents);
}
NotifierButtonWrapperView::~NotifierButtonWrapperView() = default;
void NotifierButtonWrapperView::Layout() {
int contents_width = width();
int contents_height = contents_->GetHeightForWidth(contents_width);
int y = std::max((height() - contents_height) / 2, 0);
contents_->SetBounds(0, y, contents_width, contents_height);
// Since normally we don't show |disabled_filter_|, initialize it lazily.
if (!contents_->enabled()) {
if (!disabled_filter_)
CreateDisabledFilter();
disabled_filter_->SetVisible(true);
gfx::Rect filter_bounds = GetContentsBounds();
filter_bounds.set_width(filter_bounds.width() - kEntryIconSize);
disabled_filter_->SetBoundsRect(filter_bounds);
} else if (disabled_filter_) {
disabled_filter_->SetVisible(false);
}
SetFocusBehavior(contents_->enabled() ? FocusBehavior::ALWAYS
: FocusBehavior::NEVER);
}
gfx::Size NotifierButtonWrapperView::CalculatePreferredSize() const {
return gfx::Size(kWidth, kNotifierButtonWrapperHeight);
}
void NotifierButtonWrapperView::GetAccessibleNodeData(
ui::AXNodeData* node_data) {
contents_->GetAccessibleNodeData(node_data);
}
void NotifierButtonWrapperView::OnFocus() {
views::View::OnFocus();
ScrollRectToVisible(GetLocalBounds());
// We render differently when focused.
SchedulePaint();
}
bool NotifierButtonWrapperView::OnKeyPressed(const ui::KeyEvent& event) {
return contents_->OnKeyPressed(event);
}
bool NotifierButtonWrapperView::OnKeyReleased(const ui::KeyEvent& event) {
return contents_->OnKeyReleased(event);
}
void NotifierButtonWrapperView::OnPaint(gfx::Canvas* canvas) {
View::OnPaint(canvas);
views::Painter::PaintFocusPainter(this, canvas, focus_painter_.get());
}
void NotifierButtonWrapperView::OnBlur() {
View::OnBlur();
// We render differently when focused.
SchedulePaint();
}
void NotifierButtonWrapperView::CreateDisabledFilter() {
DCHECK(!disabled_filter_);
disabled_filter_ = new views::View;
disabled_filter_->SetBackground(
views::CreateSolidBackground(kDisabledNotifierFilterColor));
disabled_filter_->set_can_process_events_within_subtree(false);
AddChildView(disabled_filter_);
}
// ScrollContentsView ----------------------------------------------------------
class ScrollContentsView : public views::View {
public:
ScrollContentsView() = default;
private:
void PaintChildren(const views::PaintInfo& paint_info) override {
views::View::PaintChildren(paint_info);
if (y() == 0)
return;
// Draw a shadow at the top of the viewport when scrolled.
const ui::PaintContext& context = paint_info.context();
gfx::Rect shadowed_area(0, 0, width(), -y());
ui::PaintRecorder recorder(context, size());
gfx::Canvas* canvas = recorder.canvas();
gfx::ShadowValues shadow;
shadow.emplace_back(
gfx::Vector2d(0, message_center_style::kScrollShadowOffsetY),
message_center_style::kScrollShadowBlur,
message_center_style::kScrollShadowColor);
cc::PaintFlags flags;
flags.setLooper(gfx::CreateShadowDrawLooper(shadow));
flags.setAntiAlias(true);
canvas->ClipRect(shadowed_area, SkClipOp::kDifference);
canvas->DrawRect(shadowed_area, flags);
}
DISALLOW_COPY_AND_ASSIGN(ScrollContentsView);
};
// EmptyNotifierView -----------------------------------------------------------
class EmptyNotifierView : public views::View {
public:
EmptyNotifierView() {
SkColor color = kUnifiedMenuTextColor;
auto layout = std::make_unique<views::BoxLayout>(
views::BoxLayout::kVertical, gfx::Insets(), 0);
layout->set_main_axis_alignment(
views::BoxLayout::MainAxisAlignment::kCenter);
layout->set_cross_axis_alignment(
views::BoxLayout::CrossAxisAlignment::kCenter);
SetLayoutManager(std::move(layout));
views::ImageView* icon = new views::ImageView();
icon->SetImage(gfx::CreateVectorIcon(kNotificationCenterEmptyIcon,
message_center_style::kEmptyIconSize,
color));
icon->SetBorder(
views::CreateEmptyBorder(message_center_style::kEmptyIconPadding));
AddChildView(icon);
views::Label* label = new views::Label(
l10n_util::GetStringUTF16(IDS_ASH_MESSAGE_CENTER_NO_NOTIFIERS));
label->SetEnabledColor(color);
label->SetAutoColorReadabilityEnabled(false);
label->SetSubpixelRenderingEnabled(false);
// "Roboto-Medium, 12sp" is specified in the mock.
label->SetFontList(
gfx::FontList().DeriveWithWeight(gfx::Font::Weight::MEDIUM));
AddChildView(label);
}
private:
DISALLOW_COPY_AND_ASSIGN(EmptyNotifierView);
};
} // namespace
// NotifierSettingsView::NotifierButton ---------------------------------------
// We do not use views::Checkbox class directly because it doesn't support
// showing 'icon'.
NotifierSettingsView::NotifierButton::NotifierButton(
const mojom::NotifierUiData& notifier_ui_data,
views::ButtonListener* listener)
: views::Button(listener),
notifier_id_(notifier_ui_data.notifier_id),
icon_view_(new views::ImageView()),
name_view_(new views::Label(notifier_ui_data.name)),
checkbox_(new views::Checkbox(base::string16(), this /* listener */)) {
name_view_->SetAutoColorReadabilityEnabled(false);
name_view_->SetEnabledColor(kUnifiedMenuTextColor);
name_view_->SetSubpixelRenderingEnabled(false);
// "Roboto-Regular, 13sp" is specified in the mock.
name_view_->SetFontList(
gfx::FontList().DeriveWithSizeDelta(kLabelFontSizeDelta));
checkbox_->SetChecked(notifier_ui_data.enabled);
checkbox_->SetFocusBehavior(FocusBehavior::NEVER);
checkbox_->SetAccessibleName(notifier_ui_data.name);
if (notifier_ui_data.enforced) {
Button::SetEnabled(false);
checkbox_->SetEnabled(false);
}
UpdateIconImage(notifier_ui_data.icon);
}
NotifierSettingsView::NotifierButton::~NotifierButton() = default;
void NotifierSettingsView::NotifierButton::UpdateIconImage(
const gfx::ImageSkia& icon) {
if (icon.isNull()) {
icon_view_->SetImage(gfx::CreateVectorIcon(
message_center::kProductIcon, kEntryIconSize, kUnifiedMenuIconColor));
} else {
icon_view_->SetImage(icon);
icon_view_->SetImageSize(gfx::Size(kEntryIconSize, kEntryIconSize));
}
GridChanged();
}
void NotifierSettingsView::NotifierButton::SetChecked(bool checked) {
checkbox_->SetChecked(checked);
}
bool NotifierSettingsView::NotifierButton::GetChecked() const {
return checkbox_->GetChecked();
}
void NotifierSettingsView::NotifierButton::ButtonPressed(
views::Button* button,
const ui::Event& event) {
DCHECK_EQ(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_->GetChecked());
Button::NotifyClick(event);
}
void NotifierSettingsView::NotifierButton::GetAccessibleNodeData(
ui::AXNodeData* node_data) {
static_cast<views::View*>(checkbox_)->GetAccessibleNodeData(node_data);
}
void NotifierSettingsView::NotifierButton::GridChanged() {
using views::ColumnSet;
using views::GridLayout;
GridLayout* layout = SetLayoutManager(std::make_unique<GridLayout>(this));
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, kInternalHorizontalSpacing);
// Add a column for the icon.
cs->AddColumn(GridLayout::CENTER, GridLayout::CENTER, 0, GridLayout::FIXED,
kEntryIconSize, 0);
cs->AddPaddingColumn(0, kSmallerInternalHorizontalSpacing);
// 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);
layout->StartRow(0, 0);
layout->AddView(checkbox_);
layout->AddView(icon_view_);
layout->AddView(name_view_);
if (!enabled()) {
views::ImageView* policy_enforced_icon = new views::ImageView();
policy_enforced_icon->SetImage(gfx::CreateVectorIcon(
kSystemMenuBusinessIcon, kEntryIconSize, kUnifiedMenuIconColor));
cs->AddColumn(GridLayout::CENTER, GridLayout::CENTER, 0, GridLayout::FIXED,
kEntryIconSize, 0);
layout->AddView(policy_enforced_icon);
}
Layout();
}
// NotifierSettingsView -------------------------------------------------------
NotifierSettingsView::NotifierSettingsView()
: quiet_mode_icon_(nullptr),
quiet_mode_toggle_(nullptr),
header_view_(nullptr),
top_label_(nullptr),
scroller_(nullptr),
no_notifiers_view_(nullptr) {
SetFocusBehavior(FocusBehavior::ACCESSIBLE_ONLY);
SetPaintToLayer();
layer()->SetFillsBoundsOpaquely(false);
header_view_ = new views::View;
header_view_->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::kVertical, kHeaderViewPadding, 0));
header_view_->SetBorder(
views::CreateSolidSidedBorder(1, 0, 0, 0, kTopBorderColor));
views::View* quiet_mode_view = new views::View;
auto* quiet_mode_layout =
quiet_mode_view->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::kHorizontal, kQuietModeViewPadding,
kQuietModeViewSpacing));
quiet_mode_icon_ = new views::ImageView();
quiet_mode_icon_->SetBorder(views::CreateEmptyBorder(kQuietModeLabelPadding));
quiet_mode_view->AddChildView(quiet_mode_icon_);
views::Label* quiet_mode_label = new views::Label(l10n_util::GetStringUTF16(
IDS_ASH_MESSAGE_CENTER_QUIET_MODE_BUTTON_TOOLTIP));
quiet_mode_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
// "Roboto-Regular, 13sp" is specified in the mock.
quiet_mode_label->SetFontList(
gfx::FontList().DeriveWithSizeDelta(kLabelFontSizeDelta));
quiet_mode_label->SetAutoColorReadabilityEnabled(false);
quiet_mode_label->SetEnabledColor(kUnifiedMenuTextColor);
quiet_mode_label->SetSubpixelRenderingEnabled(false);
quiet_mode_label->SetBorder(views::CreateEmptyBorder(kQuietModeLabelPadding));
quiet_mode_view->AddChildView(quiet_mode_label);
quiet_mode_layout->SetFlexForView(quiet_mode_label, 1);
quiet_mode_toggle_ = new views::ToggleButton(this);
quiet_mode_toggle_->SetAccessibleName(l10n_util::GetStringUTF16(
IDS_ASH_MESSAGE_CENTER_QUIET_MODE_BUTTON_TOOLTIP));
quiet_mode_toggle_->SetBorder(
views::CreateEmptyBorder(kQuietModeTogglePadding));
quiet_mode_toggle_->EnableCanvasFlippingForRTLUI(true);
SetQuietModeState(MessageCenter::Get()->IsQuietMode());
quiet_mode_view->AddChildView(quiet_mode_toggle_);
header_view_->AddChildView(quiet_mode_view);
top_label_ = new views::Label(l10n_util::GetStringUTF16(
IDS_ASH_MESSAGE_CENTER_SETTINGS_DIALOG_DESCRIPTION));
top_label_->SetBorder(views::CreateEmptyBorder(kTopLabelPadding));
// "Roboto-Medium, 13sp" is specified in the mock.
top_label_->SetFontList(gfx::FontList().Derive(
kLabelFontSizeDelta, gfx::Font::NORMAL, gfx::Font::Weight::MEDIUM));
top_label_->SetAutoColorReadabilityEnabled(false);
top_label_->SetEnabledColor(kUnifiedMenuTextColor);
top_label_->SetSubpixelRenderingEnabled(false);
top_label_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
top_label_->SetMultiLine(true);
header_view_->AddChildView(top_label_);
AddChildView(header_view_);
scroller_ = new views::ScrollView();
scroller_->SetBackgroundColor(SK_ColorTRANSPARENT);
scroller_->SetVerticalScrollBar(new views::OverlayScrollBar(false));
scroller_->SetHorizontalScrollBar(new views::OverlayScrollBar(true));
scroller_->set_draw_overflow_indicator(false);
AddChildView(scroller_);
no_notifiers_view_ = new EmptyNotifierView();
AddChildView(no_notifiers_view_);
OnNotifierListUpdated({});
Shell::Get()->message_center_controller()->AddNotifierSettingsListener(this);
Shell::Get()->message_center_controller()->RequestNotifierSettingsUpdate();
}
NotifierSettingsView::~NotifierSettingsView() {
Shell::Get()->message_center_controller()->RemoveNotifierSettingsListener(
this);
}
bool NotifierSettingsView::IsScrollable() {
return scroller_->height() < scroller_->contents()->height();
}
void NotifierSettingsView::SetQuietModeState(bool is_quiet_mode) {
quiet_mode_toggle_->SetIsOn(is_quiet_mode);
if (is_quiet_mode) {
quiet_mode_icon_->SetImage(
gfx::CreateVectorIcon(kNotificationCenterDoNotDisturbOnIcon,
kMenuIconSize, kUnifiedMenuIconColor));
} else {
quiet_mode_icon_->SetImage(
gfx::CreateVectorIcon(kNotificationCenterDoNotDisturbOffIcon,
kMenuIconSize, kUnifiedMenuIconColorDisabled));
}
}
void NotifierSettingsView::GetAccessibleNodeData(ui::AXNodeData* node_data) {
node_data->role = ax::mojom::Role::kList;
node_data->SetName(l10n_util::GetStringUTF16(
IDS_ASH_MESSAGE_CENTER_SETTINGS_DIALOG_DESCRIPTION));
}
void NotifierSettingsView::OnNotifierListUpdated(
const std::vector<mojom::NotifierUiDataPtr>& ui_data) {
// TODO(tetsui): currently notifier settings list doesn't update after once
// it's loaded, in order to retain scroll position.
if (scroller_->contents() && buttons_.size() > 0)
return;
buttons_.clear();
auto contents_view = std::make_unique<ScrollContentsView>();
contents_view->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::kVertical, gfx::Insets(0, kHorizontalMargin)));
size_t notifier_count = ui_data.size();
for (size_t i = 0; i < notifier_count; ++i) {
NotifierButton* button = new NotifierButton(*ui_data[i], this);
NotifierButtonWrapperView* wrapper = new NotifierButtonWrapperView(button);
wrapper->SetFocusBehavior(FocusBehavior::ALWAYS);
contents_view->AddChildView(wrapper);
buttons_.insert(button);
}
top_label_->SetVisible(notifier_count > 0);
no_notifiers_view_->SetVisible(notifier_count == 0);
top_label_->InvalidateLayout();
auto* contents_view_ptr = scroller_->SetContents(std::move(contents_view));
contents_view_ptr->SetBoundsRect(
gfx::Rect(contents_view_ptr->GetPreferredSize()));
Layout();
}
void NotifierSettingsView::UpdateNotifierIcon(const NotifierId& notifier_id,
const gfx::ImageSkia& icon) {
for (auto* button : buttons_) {
if (button->notifier_id() == notifier_id) {
button->UpdateIconImage(icon);
return;
}
}
}
void NotifierSettingsView::Layout() {
int header_height = header_view_->GetHeightForWidth(width());
header_view_->SetBounds(0, 0, width(), header_height);
views::View* contents_view = scroller_->contents();
int content_width = width();
int content_height = contents_view->GetHeightForWidth(content_width);
if (header_height + content_height > height()) {
content_width -= scroller_->GetScrollBarLayoutWidth();
content_height = contents_view->GetHeightForWidth(content_width);
}
contents_view->SetBounds(0, 0, content_width, content_height);
scroller_->SetBounds(0, header_height, width(), height() - header_height);
no_notifiers_view_->SetBounds(0, header_height, width(),
height() - header_height);
}
gfx::Size NotifierSettingsView::GetMinimumSize() const {
gfx::Size size(kWidth, kMinimumHeight);
int total_height = header_view_->GetPreferredSize().height() +
scroller_->contents()->GetPreferredSize().height();
if (total_height > kMinimumHeight)
size.Enlarge(scroller_->GetScrollBarLayoutWidth(), 0);
return size;
}
gfx::Size NotifierSettingsView::CalculatePreferredSize() const {
gfx::Size header_size = header_view_->GetPreferredSize();
gfx::Size content_size = scroller_->contents()->GetPreferredSize();
int no_notifiers_height = 0;
if (no_notifiers_view_->visible())
no_notifiers_height = no_notifiers_view_->GetPreferredSize().height();
return gfx::Size(
std::max(header_size.width(), content_size.width()),
std::max(kMinimumHeight, header_size.height() + content_size.height() +
no_notifiers_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 == quiet_mode_toggle_) {
MessageCenter::Get()->SetQuietMode(quiet_mode_toggle_->GetIsOn());
return;
}
auto iter = buttons_.find(static_cast<NotifierButton*>(sender));
if (iter == buttons_.end())
return;
NotifierButton* button = *iter;
button->SetChecked(!button->GetChecked());
Shell::Get()->message_center_controller()->SetNotifierEnabled(
button->notifier_id(), button->GetChecked());
Shell::Get()->message_center_controller()->RequestNotifierSettingsUpdate();
}
} // namespace ash