blob: 3263c170bacf4b6448c7283e11047649471a7e86 [file] [log] [blame]
// Copyright (c) 2012 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/tray/tray_bubble_view.h"
#include <algorithm>
#include <numeric>
#include "ash/public/cpp/ash_features.h"
#include "base/macros.h"
#include "third_party/skia/include/core/SkCanvas.h"
#include "third_party/skia/include/core/SkColor.h"
#include "third_party/skia/include/core/SkPath.h"
#include "third_party/skia/include/effects/SkBlurImageFilter.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/aura/env.h"
#include "ui/aura/window.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_owner.h"
#include "ui/events/event.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/skia_util.h"
#include "ui/native_theme/native_theme_dark_aura.h"
#include "ui/views/bubble/bubble_frame_view.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/painter.h"
#include "ui/views/views_delegate.h"
#include "ui/views/widget/widget.h"
#include "ui/wm/core/shadow_types.h"
#include "ui/wm/core/window_util.h"
using views::BubbleBorder;
using views::BubbleFrameView;
using views::NonClientFrameView;
using views::View;
using views::ViewsDelegate;
using views::Widget;
namespace ash {
namespace {
BubbleBorder::Arrow GetArrowAlignment(ash::ShelfAlignment alignment) {
// The tray bubble is in a corner. In this case, we want the arrow to be
// flush with one side instead of centered on the bubble.
switch (alignment) {
case ash::SHELF_ALIGNMENT_BOTTOM:
case ash::SHELF_ALIGNMENT_BOTTOM_LOCKED:
return base::i18n::IsRTL() ? BubbleBorder::BOTTOM_LEFT
: BubbleBorder::BOTTOM_RIGHT;
case ash::SHELF_ALIGNMENT_LEFT:
return BubbleBorder::LEFT_BOTTOM;
case ash::SHELF_ALIGNMENT_RIGHT:
return BubbleBorder::RIGHT_BOTTOM;
}
}
// Only one TrayBubbleView is visible at a time, but there are cases where the
// lifetimes of two different bubbles can overlap briefly.
int g_current_tray_bubble_showing_count_ = 0;
// Detects any mouse movement. This is needed to detect mouse movements by the
// user over the bubble if the bubble got created underneath the cursor.
class MouseMoveDetectorHost : public views::MouseWatcherHost {
public:
MouseMoveDetectorHost();
~MouseMoveDetectorHost() override;
bool Contains(const gfx::Point& screen_point, EventType type) override;
private:
DISALLOW_COPY_AND_ASSIGN(MouseMoveDetectorHost);
};
MouseMoveDetectorHost::MouseMoveDetectorHost() {}
MouseMoveDetectorHost::~MouseMoveDetectorHost() {}
bool MouseMoveDetectorHost::Contains(const gfx::Point& screen_point,
EventType type) {
return false;
}
// Custom layout for the bubble-view. Does the default box-layout if there is
// enough height. Otherwise, makes sure the bottom rows are visible.
class BottomAlignedBoxLayout : public views::BoxLayout {
public:
explicit BottomAlignedBoxLayout(TrayBubbleView* bubble_view)
: BoxLayout(BoxLayout::kVertical), bubble_view_(bubble_view) {}
~BottomAlignedBoxLayout() override {}
private:
void Layout(View* host) override {
if (host->height() >= host->GetPreferredSize().height() ||
!bubble_view_->is_gesture_dragging()) {
BoxLayout::Layout(host);
return;
}
int consumed_height = 0;
for (auto i = host->children().rbegin();
i != host->children().rend() && consumed_height < host->height();
++i) {
View* child = *i;
if (!child->GetVisible())
continue;
gfx::Size size = child->GetPreferredSize();
child->SetBounds(0, host->height() - consumed_height - size.height(),
host->width(), size.height());
consumed_height += size.height();
}
}
TrayBubbleView* bubble_view_;
DISALLOW_COPY_AND_ASSIGN(BottomAlignedBoxLayout);
};
} // namespace
TrayBubbleView::Delegate::~Delegate() {}
void TrayBubbleView::Delegate::BubbleViewDestroyed() {}
void TrayBubbleView::Delegate::OnMouseEnteredView() {}
void TrayBubbleView::Delegate::OnMouseExitedView() {}
base::string16 TrayBubbleView::Delegate::GetAccessibleNameForBubble() {
return base::string16();
}
bool TrayBubbleView::Delegate::ShouldEnableExtraKeyboardAccessibility() {
return false;
}
void TrayBubbleView::Delegate::HideBubble(const TrayBubbleView* bubble_view) {}
TrayBubbleView::InitParams::InitParams() = default;
TrayBubbleView::InitParams::InitParams(const InitParams& other) = default;
TrayBubbleView::RerouteEventHandler::RerouteEventHandler(
TrayBubbleView* tray_bubble_view)
: tray_bubble_view_(tray_bubble_view) {
aura::Env::GetInstance()->AddPreTargetHandler(
this, ui::EventTarget::Priority::kSystem);
}
TrayBubbleView::RerouteEventHandler::~RerouteEventHandler() {
aura::Env::GetInstance()->RemovePreTargetHandler(this);
}
void TrayBubbleView::RerouteEventHandler::OnKeyEvent(ui::KeyEvent* event) {
// Do not handle a key event if it is targeted to the tray or its descendants,
// or if the target has the tray as a transient ancestor. RerouteEventHandler
// is for rerouting events which are not targetted to the tray. Those events
// should be handled by the target.
aura::Window* target = static_cast<aura::Window*>(event->target());
aura::Window* tray_window = tray_bubble_view_->GetWidget()->GetNativeView();
if (target && (tray_window->Contains(target) ||
wm::HasTransientAncestor(target, tray_window))) {
return;
}
// Only passes Tab, Shift+Tab, Esc to the widget as it can consume more key
// events. e.g. Alt+Tab can be consumed as focus traversal by FocusManager.
ui::KeyboardCode key_code = event->key_code();
int flags = event->flags() &
(ui::EF_SHIFT_DOWN | ui::EF_CONTROL_DOWN | ui::EF_ALT_DOWN |
ui::EF_COMMAND_DOWN | ui::EF_ALTGR_DOWN | ui::EF_MOD3_DOWN);
if ((key_code == ui::VKEY_TAB && flags == ui::EF_NONE) ||
(key_code == ui::VKEY_TAB && flags == ui::EF_SHIFT_DOWN) ||
(key_code == ui::VKEY_ESCAPE && flags == ui::EF_NONE)) {
// Make TrayBubbleView activatable as the following Widget::OnKeyEvent might
// try to activate it.
tray_bubble_view_->SetCanActivate(true);
tray_bubble_view_->GetWidget()->OnKeyEvent(event);
if (event->handled())
return;
}
// Always consumes key event not to pass it to other widgets. Calling
// StopPropagation here to make this consistent with
// MenuController::OnWillDispatchKeyEvent.
event->StopPropagation();
// To provide consistent behavior with a menu, process accelerator as a menu
// is open if the event is not handled by the widget.
ui::Accelerator accelerator(*event);
ViewsDelegate::ProcessMenuAcceleratorResult result =
ViewsDelegate::GetInstance()->ProcessAcceleratorWhileMenuShowing(
accelerator);
if (result == ViewsDelegate::ProcessMenuAcceleratorResult::CLOSE_MENU)
tray_bubble_view_->CloseBubbleView();
}
TrayBubbleView::TrayBubbleView(const InitParams& init_params)
: BubbleDialogDelegateView(init_params.anchor_view,
GetArrowAlignment(init_params.shelf_alignment)),
params_(init_params),
layout_(nullptr),
delegate_(init_params.delegate),
preferred_width_(init_params.min_width),
bubble_border_(new BubbleBorder(
arrow(),
init_params.has_shadow ? BubbleBorder::NO_ASSETS
: BubbleBorder::BIG_SHADOW,
init_params.bg_color.value_or(gfx::kPlaceholderColor))),
owned_bubble_border_(bubble_border_),
is_gesture_dragging_(false),
mouse_actively_entered_(false) {
DCHECK(delegate_);
DCHECK(params_.parent_window);
// anchor_widget() is computed by BubbleDialogDelegateView().
DCHECK(((init_params.anchor_mode != TrayBubbleView::AnchorMode::kView) ||
anchor_widget()));
bubble_border_->set_use_theme_background_color(!init_params.bg_color);
if (init_params.corner_radius)
bubble_border_->SetCornerRadius(init_params.corner_radius.value());
bubble_border_->set_avoid_shadow_overlap(true);
set_parent_window(params_.parent_window);
SetCanActivate(false);
set_notify_enter_exit_on_child(true);
set_close_on_deactivate(init_params.close_on_deactivate);
set_margins(gfx::Insets());
SetPaintToLayer();
if (init_params.use_native_theme)
SetNativeTheme(ui::NativeThemeDarkAura::instance());
if (!ash::features::ShouldUseShaderRoundedCorner()) {
bubble_content_mask_ = views::Painter::CreatePaintedLayer(
views::Painter::CreateSolidRoundRectPainter(
SK_ColorBLACK, bubble_border_->corner_radius()));
}
auto layout = std::make_unique<BottomAlignedBoxLayout>(this);
layout->SetDefaultFlex(1);
layout_ = SetLayoutManager(std::move(layout));
if (init_params.anchor_mode == AnchorMode::kRect) {
SetAnchorView(nullptr);
SetAnchorRect(init_params.anchor_rect);
if (init_params.insets.has_value())
bubble_border_->set_insets(init_params.insets.value());
}
}
TrayBubbleView::~TrayBubbleView() {
mouse_watcher_.reset();
if (delegate_) {
// Inform host items (models) that their views are being destroyed.
delegate_->BubbleViewDestroyed();
}
}
// static
bool TrayBubbleView::IsATrayBubbleOpen() {
return g_current_tray_bubble_showing_count_ > 0;
}
void TrayBubbleView::InitializeAndShowBubble() {
if (ash::features::ShouldUseShaderRoundedCorner()) {
int radius = bubble_border_->corner_radius();
layer()->parent()->SetRoundedCornerRadius({radius, radius, radius, radius});
layer()->parent()->SetIsFastRoundedCorner(true);
} else {
CHECK(bubble_content_mask_);
layer()->parent()->SetMaskLayer(bubble_content_mask_->layer());
}
GetWidget()->Show();
UpdateBubble();
if (IsAnchoredToStatusArea())
++g_current_tray_bubble_showing_count_;
// If TrayBubbleView cannot be activated and is shown by clicking on the
// corresponding tray view, register pre target event handler to reroute key
// events to the widget for activating the view or closing it.
if (!CanActivate() && params_.show_by_click)
reroute_event_handler_ = std::make_unique<RerouteEventHandler>(this);
}
void TrayBubbleView::UpdateBubble() {
if (GetWidget()) {
SizeToContents();
GetWidget()->GetRootView()->SchedulePaint();
// When extra keyboard accessibility is enabled, focus the default item if
// no item is focused.
if (delegate_ && delegate_->ShouldEnableExtraKeyboardAccessibility())
FocusDefaultIfNeeded();
}
}
void TrayBubbleView::SetMaxHeight(int height) {
params_.max_height = height;
if (GetWidget())
SizeToContents();
}
void TrayBubbleView::SetBottomPadding(int padding) {
layout_->set_inside_border_insets(gfx::Insets(0, 0, padding, 0));
}
void TrayBubbleView::SetWidth(int width) {
width = std::max(std::min(width, params_.max_width), params_.min_width);
if (preferred_width_ == width)
return;
preferred_width_ = width;
if (GetWidget())
SizeToContents();
}
gfx::Insets TrayBubbleView::GetBorderInsets() const {
return bubble_border_->GetInsets();
}
void TrayBubbleView::ResetDelegate() {
reroute_event_handler_.reset();
delegate_ = nullptr;
}
void TrayBubbleView::ChangeAnchorView(views::View* anchor_view) {
DCHECK_EQ(AnchorMode::kView, params_.anchor_mode);
BubbleDialogDelegateView::SetAnchorView(anchor_view);
}
void TrayBubbleView::ChangeAnchorRect(const gfx::Rect& rect) {
DCHECK_EQ(AnchorMode::kRect, params_.anchor_mode);
BubbleDialogDelegateView::SetAnchorRect(rect);
}
void TrayBubbleView::ChangeAnchorAlignment(ShelfAlignment alignment) {
SetArrow(GetArrowAlignment(alignment));
}
bool TrayBubbleView::IsAnchoredToStatusArea() const {
return true;
}
int TrayBubbleView::GetDialogButtons() const {
return ui::DIALOG_BUTTON_NONE;
}
ax::mojom::Role TrayBubbleView::GetAccessibleWindowRole() {
// We override the role because the base class sets it to alert dialog.
// This would make screen readers announce the whole of the system tray
// which is undesirable.
return ax::mojom::Role::kDialog;
}
void TrayBubbleView::SizeToContents() {
BubbleDialogDelegateView::SizeToContents();
if (bubble_content_mask_)
bubble_content_mask_->layer()->SetBounds(layer()->parent()->bounds());
}
void TrayBubbleView::OnBeforeBubbleWidgetInit(Widget::InitParams* params,
Widget* bubble_widget) const {
if (bubble_border_->shadow() == BubbleBorder::NO_ASSETS) {
// Apply a WM-provided shadow (see ui/wm/core/).
params->shadow_type = Widget::InitParams::SHADOW_TYPE_DROP;
params->shadow_elevation = wm::kShadowElevationActiveWindow;
}
}
void TrayBubbleView::OnWidgetClosing(Widget* widget) {
// We no longer need to watch key events for activation if the widget is
// closing.
reroute_event_handler_.reset();
BubbleDialogDelegateView::OnWidgetClosing(widget);
if (IsAnchoredToStatusArea()) {
--g_current_tray_bubble_showing_count_;
}
DCHECK_GE(g_current_tray_bubble_showing_count_, 0)
<< "Closing " << widget->GetName();
}
void TrayBubbleView::OnWidgetActivationChanged(Widget* widget, bool active) {
// We no longer need to watch key events for activation if the widget is
// activated.
reroute_event_handler_.reset();
BubbleDialogDelegateView::OnWidgetActivationChanged(widget, active);
}
NonClientFrameView* TrayBubbleView::CreateNonClientFrameView(Widget* widget) {
BubbleFrameView* frame = static_cast<BubbleFrameView*>(
BubbleDialogDelegateView::CreateNonClientFrameView(widget));
frame->SetBubbleBorder(std::move(owned_bubble_border_));
return frame;
}
bool TrayBubbleView::WidgetHasHitTestMask() const {
return true;
}
void TrayBubbleView::GetWidgetHitTestMask(SkPath* mask) const {
DCHECK(mask);
mask->addRect(gfx::RectToSkRect(GetBubbleFrameView()->GetContentsBounds()));
}
base::string16 TrayBubbleView::GetAccessibleWindowTitle() const {
if (delegate_)
return delegate_->GetAccessibleNameForBubble();
else
return base::string16();
}
gfx::Size TrayBubbleView::CalculatePreferredSize() const {
DCHECK_LE(preferred_width_, params_.max_width);
return gfx::Size(preferred_width_, GetHeightForWidth(preferred_width_));
}
int TrayBubbleView::GetHeightForWidth(int width) const {
width = std::max(width - GetInsets().width(), 0);
const auto visible_height = [width](int height, const views::View* child) {
return height + (child->GetVisible() ? child->GetHeightForWidth(width) : 0);
};
const int height = std::accumulate(children().cbegin(), children().cend(),
GetInsets().height(), visible_height);
return (params_.max_height != 0) ? std::min(height, params_.max_height)
: height;
}
void TrayBubbleView::OnMouseEntered(const ui::MouseEvent& event) {
mouse_watcher_.reset();
if (delegate_ && !(event.flags() & ui::EF_IS_SYNTHESIZED)) {
// The user actively moved the mouse over the bubble; inform the delegate.
delegate_->OnMouseEnteredView();
mouse_actively_entered_ = true;
} else {
// The mouse was located over the bubble when it was first shown; use
// MouseWatcher to wait for user interaction before signaling the delegate.
mouse_watcher_ = std::make_unique<views::MouseWatcher>(
std::make_unique<MouseMoveDetectorHost>(), this);
mouse_watcher_->set_notify_on_exit_time(base::TimeDelta());
mouse_watcher_->Start(GetWidget()->GetNativeWindow());
}
}
void TrayBubbleView::OnMouseExited(const ui::MouseEvent& event) {
// Disable any MouseWatcher waiting for user interaction inside the bubble.
mouse_watcher_.reset();
// Only notify the delegate on exit if it was notified on enter.
if (delegate_ && mouse_actively_entered_)
delegate_->OnMouseExitedView();
}
void TrayBubbleView::GetAccessibleNodeData(ui::AXNodeData* node_data) {
if (delegate_ && CanActivate()) {
node_data->role = ax::mojom::Role::kWindow;
node_data->SetName(delegate_->GetAccessibleNameForBubble());
}
}
const char* TrayBubbleView::GetClassName() const {
return "TrayBubbleView";
}
void TrayBubbleView::MouseMovedOutOfHost() {
// The user moved the mouse that was over the bubble when it was first shown.
if (delegate_)
delegate_->OnMouseEnteredView();
mouse_actively_entered_ = true;
mouse_watcher_.reset();
}
void TrayBubbleView::ChildPreferredSizeChanged(View* child) {
SizeToContents();
}
void TrayBubbleView::ViewHierarchyChanged(
const views::ViewHierarchyChangedDetails& details) {
if (details.is_add && details.child == this) {
details.parent->SetPaintToLayer();
details.parent->layer()->SetMasksToBounds(true);
}
}
void TrayBubbleView::CloseBubbleView() {
if (!delegate_)
return;
delegate_->HideBubble(this);
}
void TrayBubbleView::FocusDefaultIfNeeded() {
views::FocusManager* manager = GetFocusManager();
if (!manager || manager->GetFocusedView())
return;
views::View* view =
manager->GetNextFocusableView(nullptr, nullptr, false, false);
if (!view)
return;
// No need to explicitly activate the widget. View::RequestFocus will activate
// it if necessary.
SetCanActivate(true);
view->RequestFocus();
}
} // namespace ash