blob: 552eca22319d5e791e7b10580286b96b94531b66 [file] [log] [blame]
// Copyright 2016 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/palette/palette_tray.h"
#include "ash/accelerators/accelerator_controller.h"
#include "ash/accessibility_delegate.h"
#include "ash/public/cpp/ash_pref_names.h"
#include "ash/public/cpp/config.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/root_window_controller.h"
#include "ash/session/session_controller.h"
#include "ash/shelf/shelf.h"
#include "ash/shelf/shelf_constants.h"
#include "ash/shell.h"
#include "ash/shell_port.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/palette/palette_tool_manager.h"
#include "ash/system/palette/palette_utils.h"
#include "ash/system/tray/system_menu_button.h"
#include "ash/system/tray/system_tray_controller.h"
#include "ash/system/tray/tray_bubble_wrapper.h"
#include "ash/system/tray/tray_constants.h"
#include "ash/system/tray/tray_container.h"
#include "ash/system/tray/tray_popup_header_button.h"
#include "ash/system/tray/tray_popup_item_style.h"
#include "ash/system/tray/tray_popup_utils.h"
#include "base/metrics/histogram_macros.h"
#include "components/prefs/pref_change_registrar.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/events/devices/input_device_manager.h"
#include "ui/events/devices/stylus_state.h"
#include "ui/events/event_constants.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/separator.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/pointer_watcher.h"
namespace ash {
namespace {
// Padding for tray icon (dp; the button that shows the palette menu).
constexpr int kTrayIconMainAxisInset = 8;
constexpr int kTrayIconCrossAxisInset = 0;
// Width of the palette itself (dp).
constexpr int kPaletteWidth = 332;
// Padding at the top/bottom of the palette (dp).
constexpr int kPalettePaddingOnTop = 4;
constexpr int kPalettePaddingOnBottom = 2;
// Margins between the title view and the edges around it (dp).
constexpr int kPaddingBetweenTitleAndLeftEdge = 12;
constexpr int kPaddingBetweenTitleAndSeparator = 3;
// Color of the separator.
const SkColor kPaletteSeparatorColor = SkColorSetARGB(0x1E, 0x00, 0x00, 0x00);
// Returns true if we are in a user session that can show the stylus tools.
bool IsInUserSession() {
SessionController* session_controller = Shell::Get()->session_controller();
return !session_controller->IsUserSessionBlocked() &&
session_controller->GetSessionState() ==
session_manager::SessionState::ACTIVE &&
!session_controller->IsKioskSession();
}
// Returns true if the |palette_tray| is on an internal display or on every
// display if requested from the command line.
bool ShouldShowOnDisplay(PaletteTray* palette_tray) {
const display::Display& display =
display::Screen::GetScreen()->GetDisplayNearestWindow(
palette_tray->GetWidget()->GetNativeWindow());
return display.IsInternal() ||
palette_utils::IsPaletteEnabledOnEveryDisplay();
}
class TitleView : public views::View, public views::ButtonListener {
public:
explicit TitleView(PaletteTray* palette_tray) : palette_tray_(palette_tray) {
// TODO(tdanderson|jdufault): Use TriView to handle the layout of the title.
// See crbug.com/614453.
auto* box_layout = new views::BoxLayout(views::BoxLayout::kHorizontal);
box_layout->set_cross_axis_alignment(
views::BoxLayout::CROSS_AXIS_ALIGNMENT_CENTER);
SetLayoutManager(box_layout);
auto* title_label =
new views::Label(l10n_util::GetStringUTF16(IDS_ASH_STYLUS_TOOLS_TITLE));
title_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
AddChildView(title_label);
TrayPopupItemStyle style(TrayPopupItemStyle::FontStyle::TITLE);
style.SetupLabel(title_label);
box_layout->SetFlexForView(title_label, 1);
help_button_ =
new SystemMenuButton(this, TrayPopupInkDropStyle::HOST_CENTERED,
kSystemMenuHelpIcon, IDS_ASH_STATUS_TRAY_HELP);
settings_button_ =
new SystemMenuButton(this, TrayPopupInkDropStyle::HOST_CENTERED,
kSystemMenuSettingsIcon, IDS_ASH_PALETTE_SETTINGS);
AddChildView(help_button_);
AddChildView(TrayPopupUtils::CreateVerticalSeparator());
AddChildView(settings_button_);
}
~TitleView() override {}
private:
// views::ButtonListener:
void ButtonPressed(views::Button* sender, const ui::Event& event) override {
if (sender == settings_button_) {
palette_tray_->RecordPaletteOptionsUsage(
PaletteTrayOptions::PALETTE_SETTINGS_BUTTON,
PaletteInvocationMethod::MENU);
Shell::Get()->system_tray_controller()->ShowPaletteSettings();
palette_tray_->HidePalette();
} else if (sender == help_button_) {
palette_tray_->RecordPaletteOptionsUsage(
PaletteTrayOptions::PALETTE_HELP_BUTTON,
PaletteInvocationMethod::MENU);
Shell::Get()->system_tray_controller()->ShowPaletteHelp();
palette_tray_->HidePalette();
} else {
NOTREACHED();
}
}
// Unowned pointers to button views so we can determine which button was
// clicked.
views::View* settings_button_;
views::View* help_button_;
PaletteTray* palette_tray_;
DISALLOW_COPY_AND_ASSIGN(TitleView);
};
} // namespace
// StylusWatcher is used to monitor for stylus events, since we only want to
// make the palette tray visible for devices without internal styluses once they
// start using the stylus.
class PaletteTray::StylusWatcher : views::PointerWatcher {
public:
explicit StylusWatcher(PrefService* pref_service)
: local_state_pref_service_(pref_service) {
ShellPort::Get()->AddPointerWatcher(this,
views::PointerWatcherEventTypes::BASIC);
}
~StylusWatcher() override { ShellPort::Get()->RemovePointerWatcher(this); }
// views::PointerWatcher:
void OnPointerEventObserved(const ui::PointerEvent& event,
const gfx::Point& location_in_screen,
gfx::NativeView target) override {
if (event.pointer_details().pointer_type ==
ui::EventPointerType::POINTER_TYPE_PEN) {
if (local_state_pref_service_)
local_state_pref_service_->SetBoolean(prefs::kHasSeenStylus, true);
}
}
private:
PrefService* local_state_pref_service_ = nullptr; // Not owned.
DISALLOW_COPY_AND_ASSIGN(StylusWatcher);
};
PaletteTray::PaletteTray(Shelf* shelf)
: TrayBackgroundView(shelf),
palette_tool_manager_(new PaletteToolManager(this)),
scoped_session_observer_(this),
weak_factory_(this) {
PaletteTool::RegisterToolInstances(palette_tool_manager_.get());
SetInkDropMode(InkDropMode::ON);
SetLayoutManager(new views::FillLayout());
icon_ = new views::ImageView();
UpdateTrayIcon();
tray_container()->SetMargin(kTrayIconMainAxisInset, kTrayIconCrossAxisInset);
tray_container()->AddChildView(icon_);
Shell::Get()->AddShellObserver(this);
ui::InputDeviceManager::GetInstance()->AddObserver(this);
if (!drag_controller())
set_drag_controller(base::MakeUnique<TrayDragController>(shelf));
}
PaletteTray::~PaletteTray() {
if (bubble_)
bubble_->bubble_view()->ResetDelegate();
ui::InputDeviceManager::GetInstance()->RemoveObserver(this);
Shell::Get()->RemoveShellObserver(this);
}
// static
void PaletteTray::RegisterLocalStatePrefs(PrefRegistrySimple* registry) {
registry->RegisterBooleanPref(prefs::kHasSeenStylus, false);
}
bool PaletteTray::ContainsPointInScreen(const gfx::Point& point) {
if (icon_ && icon_->GetBoundsInScreen().Contains(point))
return true;
return bubble_ && bubble_->bubble_view()->GetBoundsInScreen().Contains(point);
}
void PaletteTray::OnSessionStateChanged(session_manager::SessionState state) {
UpdateIconVisibility();
}
void PaletteTray::OnLockStateChanged(bool locked) {
UpdateIconVisibility();
if (locked) {
palette_tool_manager_->DisableActiveTool(PaletteGroup::MODE);
// The user can eject the stylus during the lock screen transition, which
// will open the palette. Make sure to close it if that happens.
HidePalette();
}
}
void PaletteTray::OnLocalStatePrefServiceInitialized(
PrefService* pref_service) {
local_state_pref_service_ = pref_service;
// May be null in mash_unittests where there is no mojo pref service.
if (!local_state_pref_service_)
return;
// If a device has an internal stylus or the flag to force stylus is set, mark
// the has seen stylus flag as true since we know the user has a stylus.
if (palette_utils::HasInternalStylus() ||
palette_utils::HasForcedStylusInput()) {
local_state_pref_service_->SetBoolean(prefs::kHasSeenStylus, true);
}
pref_change_registrar_ = base::MakeUnique<PrefChangeRegistrar>();
pref_change_registrar_->Init(local_state_pref_service_);
pref_change_registrar_->Add(
prefs::kHasSeenStylus,
base::Bind(&PaletteTray::OnHasSeenStylusPrefChanged,
base::Unretained(this)));
OnHasSeenStylusPrefChanged();
}
void PaletteTray::ClickedOutsideBubble() {
if (num_actions_in_bubble_ == 0) {
RecordPaletteOptionsUsage(PaletteTrayOptions::PALETTE_CLOSED_NO_ACTION,
PaletteInvocationMethod::MENU);
}
HidePalette();
}
base::string16 PaletteTray::GetAccessibleNameForTray() {
return l10n_util::GetStringUTF16(IDS_ASH_STYLUS_TOOLS_TITLE);
}
void PaletteTray::HideBubbleWithView(const views::TrayBubbleView* bubble_view) {
if (bubble_->bubble_view() == bubble_view)
HidePalette();
}
void PaletteTray::OnTouchscreenDeviceConfigurationChanged() {
UpdateIconVisibility();
}
void PaletteTray::OnStylusStateChanged(ui::StylusState stylus_state) {
// Device may have a stylus but it has been forcibly disabled.
if (!palette_utils::HasStylusInput())
return;
PaletteDelegate* palette_delegate = Shell::Get()->palette_delegate();
// Don't do anything if the palette should not be shown or if the user has
// disabled it all-together.
if (!IsInUserSession() || !palette_delegate->ShouldShowPalette())
return;
// Auto show/hide the palette if allowed by the user.
if (palette_delegate->ShouldAutoOpenPalette()) {
if (stylus_state == ui::StylusState::REMOVED && !bubble_) {
is_bubble_auto_opened_ = true;
ShowBubble();
} else if (stylus_state == ui::StylusState::INSERTED && bubble_) {
HidePalette();
}
}
// Disable any active modes if the stylus has been inserted.
if (stylus_state == ui::StylusState::INSERTED)
palette_tool_manager_->DisableActiveTool(PaletteGroup::MODE);
}
void PaletteTray::BubbleViewDestroyed() {
palette_tool_manager_->NotifyViewsDestroyed();
// The tray button remains active if the current active tool is a mode.
SetIsActive(palette_tool_manager_->GetActiveTool(PaletteGroup::MODE) !=
PaletteToolId::NONE);
}
void PaletteTray::OnMouseEnteredView() {}
void PaletteTray::OnMouseExitedView() {}
void PaletteTray::RegisterAccelerators(
const std::vector<ui::Accelerator>& accelerators,
views::TrayBubbleView* tray_bubble_view) {
Shell::Get()->accelerator_controller()->Register(accelerators,
tray_bubble_view);
}
void PaletteTray::UnregisterAllAccelerators(
views::TrayBubbleView* tray_bubble_view) {
Shell::Get()->accelerator_controller()->UnregisterAll(tray_bubble_view);
}
base::string16 PaletteTray::GetAccessibleNameForBubble() {
return GetAccessibleNameForTray();
}
bool PaletteTray::ShouldEnableExtraKeyboardAccessibility() {
return Shell::Get()->accessibility_delegate()->IsSpokenFeedbackEnabled();
}
void PaletteTray::HideBubble(const views::TrayBubbleView* bubble_view) {
HideBubbleWithView(bubble_view);
}
void PaletteTray::HidePalette() {
is_bubble_auto_opened_ = false;
num_actions_in_bubble_ = 0;
bubble_.reset();
shelf()->UpdateAutoHideState();
}
void PaletteTray::HidePaletteImmediately() {
if (bubble_)
bubble_->bubble_widget()->SetVisibilityChangedAnimationsEnabled(false);
HidePalette();
}
void PaletteTray::RecordPaletteOptionsUsage(PaletteTrayOptions option,
PaletteInvocationMethod method) {
DCHECK_NE(option, PaletteTrayOptions::PALETTE_OPTIONS_COUNT);
if (method == PaletteInvocationMethod::SHORTCUT) {
UMA_HISTOGRAM_ENUMERATION("Ash.Shelf.Palette.Usage.Shortcut", option,
PaletteTrayOptions::PALETTE_OPTIONS_COUNT);
} else if (is_bubble_auto_opened_) {
UMA_HISTOGRAM_ENUMERATION("Ash.Shelf.Palette.Usage.AutoOpened", option,
PaletteTrayOptions::PALETTE_OPTIONS_COUNT);
} else {
UMA_HISTOGRAM_ENUMERATION("Ash.Shelf.Palette.Usage", option,
PaletteTrayOptions::PALETTE_OPTIONS_COUNT);
}
}
void PaletteTray::RecordPaletteModeCancellation(PaletteModeCancelType type) {
if (type == PaletteModeCancelType::PALETTE_MODE_CANCEL_TYPE_COUNT)
return;
UMA_HISTOGRAM_ENUMERATION(
"Ash.Shelf.Palette.ModeCancellation", type,
PaletteModeCancelType::PALETTE_MODE_CANCEL_TYPE_COUNT);
}
void PaletteTray::OnActiveToolChanged() {
++num_actions_in_bubble_;
// If there is no tool currently active and the palette tray button was active
// (eg. a mode was deactivated without pressing the palette tray button), make
// the palette tray button inactive.
if (palette_tool_manager_->GetActiveTool(PaletteGroup::MODE) ==
PaletteToolId::NONE &&
is_active()) {
SetIsActive(false);
}
UpdateTrayIcon();
}
aura::Window* PaletteTray::GetWindow() {
return shelf()->GetWindow();
}
void PaletteTray::AnchorUpdated() {
if (bubble_)
bubble_->bubble_view()->UpdateBubble();
}
void PaletteTray::Initialize() {
PaletteDelegate* delegate = Shell::Get()->palette_delegate();
// |delegate| can be null in tests.
if (!delegate)
return;
// OnPaletteEnabledPrefChanged will get called with the initial pref value,
// which will take care of showing the palette.
palette_enabled_subscription_ = delegate->AddPaletteEnableListener(base::Bind(
&PaletteTray::OnPaletteEnabledPrefChanged, weak_factory_.GetWeakPtr()));
}
bool PaletteTray::PerformAction(const ui::Event& event) {
if (bubble_) {
if (num_actions_in_bubble_ == 0) {
RecordPaletteOptionsUsage(PaletteTrayOptions::PALETTE_CLOSED_NO_ACTION,
PaletteInvocationMethod::MENU);
}
HidePalette();
return true;
}
// Deactivate the active tool if there is one.
PaletteToolId active_tool_id =
palette_tool_manager_->GetActiveTool(PaletteGroup::MODE);
if (active_tool_id != PaletteToolId::NONE) {
palette_tool_manager_->DeactivateTool(active_tool_id);
// TODO(sammiequon): Investigate whether we should removed |is_switched|
// from PaletteToolIdToPaletteModeCancelType.
RecordPaletteModeCancellation(PaletteToolIdToPaletteModeCancelType(
active_tool_id, false /*is_switched*/));
SetIsActive(false);
return true;
}
ShowBubble();
return true;
}
void PaletteTray::CloseBubble() {
HidePalette();
}
void PaletteTray::ShowBubble() {
if (bubble_)
return;
DCHECK(tray_container());
views::TrayBubbleView::InitParams init_params;
init_params.delegate = this;
init_params.parent_window = GetBubbleWindowContainer();
init_params.anchor_view = GetBubbleAnchor();
init_params.anchor_alignment = GetAnchorAlignment();
init_params.min_width = kPaletteWidth;
init_params.max_width = kPaletteWidth;
init_params.close_on_deactivate = true;
// TODO(tdanderson): Refactor into common row layout code.
// TODO(tdanderson|jdufault): Add material design ripple effects to the menu
// rows.
// Create and customize bubble view.
views::TrayBubbleView* bubble_view = new views::TrayBubbleView(init_params);
bubble_view->set_anchor_view_insets(GetBubbleAnchorInsets());
bubble_view->set_margins(
gfx::Insets(kPalettePaddingOnTop, 0, kPalettePaddingOnBottom, 0));
// Add title.
auto* title_view = new TitleView(this);
title_view->SetBorder(views::CreateEmptyBorder(
gfx::Insets(0, kPaddingBetweenTitleAndLeftEdge, 0, 0)));
bubble_view->AddChildView(title_view);
// Add horizontal separator between the title and tools.
auto* separator = new views::Separator();
separator->SetColor(kPaletteSeparatorColor);
separator->SetBorder(views::CreateEmptyBorder(gfx::Insets(
kPaddingBetweenTitleAndSeparator, 0, kMenuSeparatorVerticalPadding, 0)));
bubble_view->AddChildView(separator);
// Add palette tools.
// TODO(tdanderson|jdufault): Use SystemMenuButton to get the material design
// ripples.
std::vector<PaletteToolView> views = palette_tool_manager_->CreateViews();
for (const PaletteToolView& view : views)
bubble_view->AddChildView(view.view);
// Show the bubble.
bubble_ = base::MakeUnique<ash::TrayBubbleWrapper>(this, bubble_view);
SetIsActive(true);
}
views::TrayBubbleView* PaletteTray::GetBubbleView() {
return bubble_ ? bubble_->bubble_view() : nullptr;
}
void PaletteTray::UpdateTrayIcon() {
icon_->SetImage(CreateVectorIcon(
palette_tool_manager_->GetActiveTrayIcon(
palette_tool_manager_->GetActiveTool(PaletteGroup::MODE)),
kTrayIconSize, kShelfIconColor));
}
void PaletteTray::OnPaletteEnabledPrefChanged(bool enabled) {
is_palette_enabled_ = enabled;
if (!enabled) {
SetVisible(false);
palette_tool_manager_->DisableActiveTool(PaletteGroup::MODE);
} else {
UpdateIconVisibility();
}
}
void PaletteTray::OnHasSeenStylusPrefChanged() {
DCHECK(local_state_pref_service_);
has_seen_stylus_ =
local_state_pref_service_->GetBoolean(prefs::kHasSeenStylus);
// On reading the pref, do not bother monitoring stylus events if the device
// has seen a stylus event before, otherwise start monitoring for stylus
// events.
// TODO(sammiequon): Investigate if we can avoid starting the watcher if the
// device is not compatible with stylus.
if (has_seen_stylus_)
watcher_.reset();
else
watcher_ = base::MakeUnique<StylusWatcher>(local_state_pref_service_);
UpdateIconVisibility();
}
void PaletteTray::UpdateIconVisibility() {
SetVisible(has_seen_stylus_ && is_palette_enabled_ &&
palette_utils::HasStylusInput() && ShouldShowOnDisplay(this) &&
IsInUserSession());
}
// TestApi. For testing purposes.
PaletteTray::TestApi::TestApi(PaletteTray* palette_tray)
: palette_tray_(palette_tray) {
DCHECK(palette_tray_);
}
PaletteTray::TestApi::~TestApi() {}
} // namespace ash