blob: dfdd5cf4f04ed0bd7a274a20ebe9b9b705e82881 [file] [log] [blame] [edit]
// 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 "ash/root_window_controller.h"
#include "ash/shell.h"
#include "ash/shell_window_ids.h"
#include "ash/system/tray/tray_constants.h"
#include "ash/wm/property_util.h"
#include "ash/wm/shelf_layout_manager.h"
#include "ash/wm/window_animations.h"
#include "grit/ash_strings.h"
#include "third_party/skia/include/core/SkCanvas.h"
#include "third_party/skia/include/core/SkColor.h"
#include "third_party/skia/include/core/SkPaint.h"
#include "third_party/skia/include/core/SkPath.h"
#include "third_party/skia/include/effects/SkBlurImageFilter.h"
#include "ui/aura/window.h"
#include "ui/base/accessibility/accessible_view_state.h"
#include "ui/base/events/event.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/insets.h"
#include "ui/gfx/screen.h"
#include "ui/views/bubble/bubble_frame_view.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/widget/widget.h"
namespace ash {
namespace {
const int kShadowThickness = 4;
const int kBottomLineHeight = 1;
const int kSystemTrayBubbleHorizontalInset = 1;
const int kSystemTrayBubbleVerticalInset = 1;
const int kArrowHeight = 9;
const int kArrowWidth = 19;
// Inset the arrow a bit from the edge.
const int kArrowEdgeMargin = 12;
const int kArrowMinOffset = kArrowWidth / 2 + kArrowEdgeMargin;
const SkColor kShadowColor = SkColorSetARGB(0xff, 0, 0, 0);
const int kAnimationDurationForPopupMS = 200;
void DrawBlurredShadowAroundView(gfx::Canvas* canvas,
int top,
int bottom,
int width,
const gfx::Insets& inset) {
SkPath path;
path.incReserve(4);
path.moveTo(SkIntToScalar(inset.left() + kShadowThickness),
SkIntToScalar(top + kShadowThickness + 1));
path.lineTo(SkIntToScalar(inset.left() + kShadowThickness),
SkIntToScalar(bottom));
path.lineTo(SkIntToScalar(width),
SkIntToScalar(bottom));
path.lineTo(SkIntToScalar(width),
SkIntToScalar(top + kShadowThickness + 1));
SkPaint paint;
paint.setColor(kShadowColor);
paint.setStyle(SkPaint::kStroke_Style);
paint.setXfermodeMode(SkXfermode::kSrcOver_Mode);
paint.setStrokeWidth(SkIntToScalar(3));
paint.setImageFilter(new SkBlurImageFilter(
SkIntToScalar(3), SkIntToScalar(3)))->unref();
canvas->sk_canvas()->drawPath(path, paint);
}
class TrayBubbleBorder : public views::BubbleBorder {
public:
TrayBubbleBorder(views::View* owner,
views::View* anchor,
views::BubbleBorder::ArrowLocation arrow_location,
int arrow_offset,
const SkColor& arrow_color)
: views::BubbleBorder(arrow_location, views::BubbleBorder::NO_SHADOW),
owner_(owner),
anchor_(anchor),
tray_arrow_offset_(arrow_offset) {
set_alignment(views::BubbleBorder::ALIGN_EDGE_TO_ANCHOR_EDGE);
set_background_color(arrow_color);
}
virtual ~TrayBubbleBorder() {}
// Overridden from views::BubbleBorder.
// Override views::BubbleBorder to set the bubble on top of the anchor when
// it has no arrow.
virtual gfx::Rect GetBounds(const gfx::Rect& position_relative_to,
const gfx::Size& contents_size) const OVERRIDE {
if (arrow_location() != NONE) {
return views::BubbleBorder::GetBounds(position_relative_to,
contents_size);
}
gfx::Size border_size(contents_size);
gfx::Insets insets;
GetInsets(&insets);
border_size.Enlarge(insets.width(), insets.height());
const int kArrowOverlap = 3;
int x = position_relative_to.x() +
position_relative_to.width() / 2 - border_size.width() / 2;
// Position the bubble on top of the anchor.
int y = position_relative_to.y() +
kArrowOverlap - border_size.height();
return gfx::Rect(x, y, border_size.width(), border_size.height());
}
// TrayBubbleView supports dynamically updated bubbles. This does not
// behave well with BubbleFrameView which expects arrow_location to be
// unmirrored during initial layout (when ClientView is constructed),
// then mirrored after SizeToContents() gets called.
// So, instead of mirroring the arrow in CreateNonClientFrameView,
// mirror it here instead.
// TODO(stevenjb): Fix this in ui/views/bubble: crbug.com/139813
virtual void GetInsets(gfx::Insets* insets) const OVERRIDE {
ArrowLocation arrow_loc = arrow_location();
if (base::i18n::IsRTL())
arrow_loc = horizontal_mirror(arrow_loc);
return GetInsetsForArrowLocation(insets, arrow_loc);
}
// Overridden from views::Border.
virtual void Paint(const views::View& view,
gfx::Canvas* canvas) const OVERRIDE {
gfx::Insets inset;
// Get the unmirrored insets for the arrow location; the tray bubbles are
// never mirrored for RTL (since that would put them off screen).
GetInsetsForArrowLocation(&inset, arrow_location());
DrawBlurredShadowAroundView(
canvas, 0, owner_->height(), owner_->width(), inset);
// Draw the bottom line.
int y = owner_->height() + inset.top();
canvas->FillRect(gfx::Rect(inset.left(), y, owner_->width(),
kBottomLineHeight), kBorderDarkColor);
if (!Shell::GetInstance()->shelf()->IsVisible() ||
arrow_location() == views::BubbleBorder::NONE)
return;
gfx::Point arrow_reference;
// Draw the arrow after drawing child borders, so that the arrow can cover
// its overlap section with child border.
SkPath path;
path.incReserve(4);
if (arrow_location() == views::BubbleBorder::BOTTOM_RIGHT ||
arrow_location() == views::BubbleBorder::BOTTOM_LEFT) {
// Note: tray_arrow_offset_ is relative to the anchor widget.
int tip_x;
if (tray_arrow_offset_ ==
internal::TrayBubbleView::InitParams::kArrowDefaultOffset) {
if (arrow_location() == views::BubbleBorder::BOTTOM_LEFT)
tip_x = kArrowMinOffset;
else
tip_x = owner_->width() - kArrowMinOffset;
} else {
gfx::Point pt(tray_arrow_offset_, 0);
views::View::ConvertPointToScreen(
anchor_->GetWidget()->GetRootView(), &pt);
views::View::ConvertPointFromScreen(
owner_->GetWidget()->GetRootView(), &pt);
tip_x = std::min(pt.x(), owner_->width() - kArrowMinOffset);
tip_x = std::max(tip_x, kArrowMinOffset);
}
int left_base_x = tip_x - kArrowWidth / 2;
int left_base_y = y;
int tip_y = left_base_y + kArrowHeight;
path.moveTo(SkIntToScalar(left_base_x), SkIntToScalar(left_base_y));
path.lineTo(SkIntToScalar(tip_x), SkIntToScalar(tip_y));
path.lineTo(SkIntToScalar(left_base_x + kArrowWidth),
SkIntToScalar(left_base_y));
arrow_reference.SetPoint(tip_x, left_base_y - kArrowHeight);
} else {
int tip_y;
if (tray_arrow_offset_ ==
internal::TrayBubbleView::InitParams::kArrowDefaultOffset) {
tip_y = owner_->height() - kArrowMinOffset;
} else {
int pty = y - tray_arrow_offset_;
gfx::Point pt(0, pty);
views::View::ConvertPointToScreen(
anchor_->GetWidget()->GetRootView(), &pt);
views::View::ConvertPointFromScreen(
owner_->GetWidget()->GetRootView(), &pt);
tip_y = std::min(pt.y(), owner_->height() - kArrowMinOffset);
tip_y = std::max(tip_y, kArrowMinOffset);
}
int top_base_y = tip_y - kArrowWidth / 2;
int top_base_x, tip_x;
if (arrow_location() == views::BubbleBorder::LEFT_BOTTOM) {
top_base_x = inset.left() + kSystemTrayBubbleHorizontalInset;
tip_x = top_base_x - kArrowHeight;
arrow_reference.SetPoint(top_base_x + kArrowHeight, tip_y);
} else {
DCHECK(arrow_location() == views::BubbleBorder::RIGHT_BOTTOM);
top_base_x = inset.left() + owner_->width() -
kSystemTrayBubbleHorizontalInset;
tip_x = top_base_x + kArrowHeight;
arrow_reference.SetPoint(top_base_x - kArrowHeight, tip_y);
}
path.moveTo(SkIntToScalar(top_base_x), SkIntToScalar(top_base_y));
path.lineTo(SkIntToScalar(tip_x), SkIntToScalar(tip_y));
path.lineTo(SkIntToScalar(top_base_x),
SkIntToScalar(top_base_y + kArrowWidth));
}
SkPaint paint;
paint.setAntiAlias(true);
paint.setStyle(SkPaint::kFill_Style);
paint.setColor(background_color());
canvas->DrawPath(path, paint);
// Now draw the arrow border.
paint.setStyle(SkPaint::kStroke_Style);
paint.setColor(kBorderDarkColor);
canvas->DrawPath(path, paint);
}
views::View* owner_;
views::View* anchor_;
const int tray_arrow_offset_;
DISALLOW_COPY_AND_ASSIGN(TrayBubbleBorder);
};
// Custom frame-view for the bubble. It overrides the following behaviour of the
// standard BubbleFrameView:
// - Sets the minimum size to an empty box.
class TrayBubbleFrameView : public views::BubbleFrameView {
public:
TrayBubbleFrameView(const gfx::Insets& margins, TrayBubbleBorder* border)
: views::BubbleFrameView(margins, border) {
}
virtual ~TrayBubbleFrameView() {}
virtual gfx::Size GetMinimumSize() OVERRIDE {
return gfx::Size();
}
private:
DISALLOW_COPY_AND_ASSIGN(TrayBubbleFrameView);
};
// 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(internal::TrayBubbleView* bubble_view)
: views::BoxLayout(views::BoxLayout::kVertical, 0, 0, 0),
bubble_view_(bubble_view) {
}
virtual ~BottomAlignedBoxLayout() {}
private:
virtual void Layout(views::View* host) OVERRIDE {
if (host->height() >= host->GetPreferredSize().height() ||
!bubble_view_->is_gesture_dragging()) {
views::BoxLayout::Layout(host);
return;
}
int consumed_height = 0;
for (int i = host->child_count() - 1;
i >= 0 && consumed_height < host->height(); --i) {
views::View* child = host->child_at(i);
if (!child->visible())
continue;
gfx::Size size = child->GetPreferredSize();
child->SetBounds(0, host->height() - consumed_height - size.height(),
host->width(), size.height());
consumed_height += size.height();
}
}
internal::TrayBubbleView* bubble_view_;
DISALLOW_COPY_AND_ASSIGN(BottomAlignedBoxLayout);
};
} // namespace
namespace internal {
// static
const int TrayBubbleView::InitParams::kArrowDefaultOffset = -1;
TrayBubbleView::InitParams::InitParams(AnchorType anchor_type,
ShelfAlignment shelf_alignment)
: anchor_type(anchor_type),
shelf_alignment(shelf_alignment),
bubble_width(kTrayPopupWidth),
max_height(0),
can_activate(false),
close_on_deactivate(true),
arrow_offset(kArrowDefaultOffset),
arrow_color(kHeaderBackgroundColorDark) {
}
TrayBubbleView* TrayBubbleView::Create(views::View* anchor,
Host* host,
const InitParams& init_params) {
// Set arrow_location here so that it can be passed correctly to the
// BubbleView constructor.
views::BubbleBorder::ArrowLocation arrow_location;
if (init_params.anchor_type == ANCHOR_TYPE_TRAY) {
if (init_params.shelf_alignment == SHELF_ALIGNMENT_BOTTOM) {
arrow_location = base::i18n::IsRTL() ?
views::BubbleBorder::BOTTOM_LEFT : views::BubbleBorder::BOTTOM_RIGHT;
} else if (init_params.shelf_alignment == SHELF_ALIGNMENT_LEFT) {
arrow_location = views::BubbleBorder::LEFT_BOTTOM;
} else {
arrow_location = views::BubbleBorder::RIGHT_BOTTOM;
}
} else {
arrow_location = views::BubbleBorder::NONE;
}
return new TrayBubbleView(init_params, arrow_location, anchor, host);
}
TrayBubbleView::TrayBubbleView(
const InitParams& init_params,
views::BubbleBorder::ArrowLocation arrow_location,
views::View* anchor,
Host* host)
: views::BubbleDelegateView(anchor, arrow_location),
params_(init_params),
host_(host),
is_gesture_dragging_(false) {
set_margins(gfx::Insets());
set_parent_window(Shell::GetContainer(
anchor->GetWidget()->GetNativeWindow()->GetRootWindow(),
internal::kShellWindowId_SettingBubbleContainer));
set_notify_enter_exit_on_child(true);
set_close_on_deactivate(init_params.close_on_deactivate);
SetPaintToLayer(true);
SetFillsBoundsOpaquely(true);
}
TrayBubbleView::~TrayBubbleView() {
// Inform host items (models) that their views are being destroyed.
if (host_)
host_->BubbleViewDestroyed();
}
void TrayBubbleView::UpdateBubble() {
SizeToContents();
GetWidget()->GetRootView()->SchedulePaint();
}
void TrayBubbleView::SetMaxHeight(int height) {
params_.max_height = height;
if (GetWidget())
SizeToContents();
}
void TrayBubbleView::Init() {
views::BoxLayout* layout = new BottomAlignedBoxLayout(this);
layout->set_spread_blank_space(true);
SetLayoutManager(layout);
set_background(NULL);
}
gfx::Rect TrayBubbleView::GetAnchorRect() {
gfx::Rect rect;
if (anchor_widget()->IsVisible()) {
rect = anchor_widget()->GetWindowBoundsInScreen();
if (params_.anchor_type == ANCHOR_TYPE_TRAY) {
if (params_.shelf_alignment == SHELF_ALIGNMENT_BOTTOM) {
bool rtl = base::i18n::IsRTL();
rect.Inset(
rtl ? kPaddingFromRightEdgeOfScreenBottomAlignment : 0,
0,
rtl ? 0 : kPaddingFromRightEdgeOfScreenBottomAlignment,
kPaddingFromBottomOfScreenBottomAlignment);
} else if (params_.shelf_alignment == SHELF_ALIGNMENT_LEFT) {
rect.Inset(0, 0, kPaddingFromInnerEdgeOfLauncherVerticalAlignment,
kPaddingFromBottomOfScreenVerticalAlignment);
} else {
rect.Inset(kPaddingFromInnerEdgeOfLauncherVerticalAlignment,
0, 0, kPaddingFromBottomOfScreenVerticalAlignment);
}
} else if (params_.anchor_type == ANCHOR_TYPE_BUBBLE) {
// Invert the offsets to align with the bubble below.
if (params_.shelf_alignment == SHELF_ALIGNMENT_LEFT) {
rect.Inset(kPaddingFromInnerEdgeOfLauncherVerticalAlignment,
0, 0, kPaddingFromBottomOfScreenVerticalAlignment);
} else if (params_.shelf_alignment == SHELF_ALIGNMENT_RIGHT) {
rect.Inset(0, 0, kPaddingFromInnerEdgeOfLauncherVerticalAlignment,
kPaddingFromBottomOfScreenVerticalAlignment);
}
}
}
// TODO(jennyz): May need to add left/right alignment in the following code.
if (rect.IsEmpty()) {
rect = gfx::Screen::GetPrimaryDisplay().bounds();
rect = gfx::Rect(
base::i18n::IsRTL() ? kPaddingFromRightEdgeOfScreenBottomAlignment :
rect.width() - kPaddingFromRightEdgeOfScreenBottomAlignment,
rect.height() - kPaddingFromBottomOfScreenBottomAlignment,
0, 0);
}
return rect;
}
gfx::Rect TrayBubbleView::GetBubbleBounds() {
// Same as BubbleDelegateView implementation, but don't try mirroring.
gfx::Size use_size = is_gesture_dragging_ ? size() : GetPreferredSize();
return GetBubbleFrameView()->GetUpdatedWindowBounds(
GetAnchorRect(), use_size, false /*try_mirroring_arrow*/);
}
bool TrayBubbleView::CanActivate() const {
return params_.can_activate;
}
// Overridden to create BubbleFrameView and set the border to TrayBubbleBorder
// (instead of creating a default BubbleBorder and replacing it).
views::NonClientFrameView* TrayBubbleView::CreateNonClientFrameView(
views::Widget* widget) {
TrayBubbleBorder* bubble_border = new TrayBubbleBorder(
this, anchor_view(),
arrow_location(), params_.arrow_offset, params_.arrow_color);
return new TrayBubbleFrameView(margins(), bubble_border);
}
gfx::Size TrayBubbleView::GetPreferredSize() {
gfx::Size size = views::BubbleDelegateView::GetPreferredSize();
int height = size.height();
if (params_.max_height != 0 && height > params_.max_height)
height = params_.max_height;
return gfx::Size(params_.bubble_width, height);
}
void TrayBubbleView::OnMouseEntered(const ui::MouseEvent& event) {
if (host_)
host_->OnMouseEnteredView();
}
void TrayBubbleView::OnMouseExited(const ui::MouseEvent& event) {
if (host_)
host_->OnMouseExitedView();
}
void TrayBubbleView::GetAccessibleState(ui::AccessibleViewState* state) {
if (params_.can_activate) {
state->role = ui::AccessibilityTypes::ROLE_WINDOW;
state->name = host_->GetAccessibleName();
}
}
void TrayBubbleView::ChildPreferredSizeChanged(View* child) {
SizeToContents();
}
void TrayBubbleView::ViewHierarchyChanged(bool is_add,
views::View* parent,
views::View* child) {
if (is_add && child == this) {
parent->SetPaintToLayer(true);
parent->SetFillsBoundsOpaquely(true);
parent->layer()->SetMasksToBounds(true);
}
}
TrayBubbleView::Host::Host()
: widget_(NULL),
tray_view_(NULL) {
Shell::GetInstance()->AddEnvEventFilter(this);
}
TrayBubbleView::Host::~Host() {
Shell::GetInstance()->RemoveEnvEventFilter(this);
}
void TrayBubbleView::Host::InitializeAndShowBubble(views::Widget* widget,
TrayBubbleView* bubble_view,
views::View* tray_view) {
widget_ = widget;
tray_view_ = tray_view;
// Must occur after call to BubbleDelegateView::CreateBubble().
bubble_view->SetAlignment(views::BubbleBorder::ALIGN_EDGE_TO_ANCHOR_EDGE);
// Setup animation.
ash::SetWindowVisibilityAnimationType(
widget->GetNativeWindow(),
ash::WINDOW_VISIBILITY_ANIMATION_TYPE_FADE);
ash::SetWindowVisibilityAnimationTransition(
widget->GetNativeWindow(),
ash::ANIMATE_BOTH);
ash::SetWindowVisibilityAnimationDuration(
widget->GetNativeWindow(),
base::TimeDelta::FromMilliseconds(kAnimationDurationForPopupMS));
bubble_view->Show();
bubble_view->UpdateBubble();
}
bool TrayBubbleView::Host::PreHandleKeyEvent(aura::Window* target,
ui::KeyEvent* event) {
return false;
}
bool TrayBubbleView::Host::PreHandleMouseEvent(aura::Window* target,
ui::MouseEvent* event) {
if (event->type() == ui::ET_MOUSE_PRESSED)
ProcessLocatedEvent(target, *event);
return false;
}
ui::TouchStatus TrayBubbleView::Host::PreHandleTouchEvent(
aura::Window* target,
ui::TouchEvent* event) {
if (event->type() == ui::ET_TOUCH_PRESSED)
ProcessLocatedEvent(target, *event);
return ui::TOUCH_STATUS_UNKNOWN;
}
ui::EventResult TrayBubbleView::Host::PreHandleGestureEvent(
aura::Window* target,
ui::GestureEvent* event) {
return ui::ER_UNHANDLED;
}
void TrayBubbleView::Host::ProcessLocatedEvent(
aura::Window* target, const ui::LocatedEvent& event) {
if (target) {
// Don't process events that occurred inside an embedded menu.
RootWindowController* root_controller =
GetRootWindowController(target->GetRootWindow());
if (root_controller && root_controller->GetContainer(
ash::internal::kShellWindowId_MenuContainer)->Contains(target)) {
return;
}
}
if (!widget_)
return;
gfx::Rect bounds = widget_->GetNativeWindow()->GetBoundsInRootWindow();
if (bounds.Contains(event.root_location()))
return;
if (tray_view_) {
// If the user clicks on the parent tray, don't process the event here,
// let the tray logic handle the event and determine show/hide behavior.
bounds = tray_view_->ConvertRectToWidget(tray_view_->GetLocalBounds());
if (bounds.Contains(event.location()))
return;
}
// Handle clicking outside the bubble and tray. We don't block the event, so
// it will also be handled by whatever widget was clicked on.
OnClickedOutsideView();
}
} // namespace internal
} // namespace ash