diff options
Diffstat (limited to 'chrome/browser/ui/views/info_bubble.cc')
-rw-r--r-- | chrome/browser/ui/views/info_bubble.cc | 550 |
1 files changed, 550 insertions, 0 deletions
diff --git a/chrome/browser/ui/views/info_bubble.cc b/chrome/browser/ui/views/info_bubble.cc new file mode 100644 index 0000000..befb716 --- /dev/null +++ b/chrome/browser/ui/views/info_bubble.cc @@ -0,0 +1,550 @@ +// Copyright (c) 2010 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 "chrome/browser/views/info_bubble.h" + +#include "app/keyboard_codes.h" +#include "app/slide_animation.h" +#include "chrome/browser/window_sizer.h" +#include "chrome/common/notification_service.h" +#include "gfx/canvas_skia.h" +#include "gfx/color_utils.h" +#include "gfx/path.h" +#include "third_party/skia/include/core/SkPaint.h" +#include "views/fill_layout.h" +#include "views/widget/root_view.h" +#include "views/widget/widget.h" +#include "views/window/client_view.h" +#include "views/window/window.h" + +#if defined(OS_CHROMEOS) +#include "chrome/browser/chromeos/wm_ipc.h" +#include "cros/chromeos_wm_ipc_enums.h" +#endif + +// How long the fade should last for. +static const int kHideFadeDurationMS = 200; + +// Background color of the bubble. +#if defined(OS_WIN) +const SkColor InfoBubble::kBackgroundColor = + color_utils::GetSysSkColor(COLOR_WINDOW); +#else +// TODO(beng): source from theme provider. +const SkColor InfoBubble::kBackgroundColor = SK_ColorWHITE; +#endif + +void BorderContents::Init() { + // Default arrow location. + BubbleBorder::ArrowLocation arrow_location = BubbleBorder::TOP_LEFT; + if (base::i18n::IsRTL()) + arrow_location = BubbleBorder::horizontal_mirror(arrow_location); + DCHECK(!bubble_border_); + bubble_border_ = new BubbleBorder(arrow_location); + set_border(bubble_border_); + bubble_border_->set_background_color(InfoBubble::kBackgroundColor); +} + +void BorderContents::SizeAndGetBounds( + const gfx::Rect& position_relative_to, + BubbleBorder::ArrowLocation arrow_location, + bool allow_bubble_offscreen, + const gfx::Size& contents_size, + gfx::Rect* contents_bounds, + gfx::Rect* window_bounds) { + if (base::i18n::IsRTL()) + arrow_location = BubbleBorder::horizontal_mirror(arrow_location); + bubble_border_->set_arrow_location(arrow_location); + // Set the border. + set_border(bubble_border_); + bubble_border_->set_background_color(InfoBubble::kBackgroundColor); + + // Give the contents a margin. + gfx::Size local_contents_size(contents_size); + local_contents_size.Enlarge(kLeftMargin + kRightMargin, + kTopMargin + kBottomMargin); + + // Try putting the arrow in its initial location, and calculating the bounds. + *window_bounds = + bubble_border_->GetBounds(position_relative_to, local_contents_size); + if (!allow_bubble_offscreen) { + gfx::Rect monitor_bounds = GetMonitorBounds(position_relative_to); + if (!monitor_bounds.IsEmpty()) { + // Try to resize vertically if this does not fit on the screen. + MirrorArrowIfOffScreen(true, // |vertical|. + position_relative_to, monitor_bounds, + local_contents_size, &arrow_location, + window_bounds); + // Then try to resize horizontally if it still does not fit on the screen. + MirrorArrowIfOffScreen(false, // |vertical|. + position_relative_to, monitor_bounds, + local_contents_size, &arrow_location, + window_bounds); + } + } + + // Calculate the bounds of the contained contents (in window coordinates) by + // subtracting the border dimensions and margin amounts. + *contents_bounds = gfx::Rect(gfx::Point(), window_bounds->size()); + gfx::Insets insets; + bubble_border_->GetInsets(&insets); + contents_bounds->Inset(insets.left() + kLeftMargin, insets.top() + kTopMargin, + insets.right() + kRightMargin, insets.bottom() + kBottomMargin); +} + +gfx::Rect BorderContents::GetMonitorBounds(const gfx::Rect& rect) { + scoped_ptr<WindowSizer::MonitorInfoProvider> monitor_provider( + WindowSizer::CreateDefaultMonitorInfoProvider()); + return monitor_provider->GetMonitorWorkAreaMatching(rect); +} + +void BorderContents::Paint(gfx::Canvas* canvas) { + // The border of this view creates an anti-aliased round-rect region for the + // contents, which we need to fill with the background color. + // NOTE: This doesn't handle an arrow location of "NONE", which has square top + // corners. + SkPaint paint; + paint.setAntiAlias(true); + paint.setStyle(SkPaint::kFill_Style); + paint.setColor(InfoBubble::kBackgroundColor); + gfx::Path path; + gfx::Rect bounds(GetLocalBounds(false)); + SkRect rect; + rect.set(SkIntToScalar(bounds.x()), SkIntToScalar(bounds.y()), + SkIntToScalar(bounds.right()), SkIntToScalar(bounds.bottom())); + SkScalar radius = SkIntToScalar(BubbleBorder::GetCornerRadius()); + path.addRoundRect(rect, radius, radius); + canvas->AsCanvasSkia()->drawPath(path, paint); + + // Now we paint the border, so it will be alpha-blended atop the contents. + // This looks slightly better in the corners than drawing the contents atop + // the border. + PaintBorder(canvas); +} + +void BorderContents::MirrorArrowIfOffScreen( + bool vertical, + const gfx::Rect& position_relative_to, + const gfx::Rect& monitor_bounds, + const gfx::Size& local_contents_size, + BubbleBorder::ArrowLocation* arrow_location, + gfx::Rect* window_bounds) { + // If the bounds don't fit, move the arrow to its mirrored position to see if + // it improves things. + gfx::Insets offscreen_insets; + if (ComputeOffScreenInsets(monitor_bounds, *window_bounds, + &offscreen_insets) && + GetInsetsLength(offscreen_insets, vertical) > 0) { + BubbleBorder::ArrowLocation original_arrow_location = *arrow_location; + *arrow_location = + vertical ? BubbleBorder::vertical_mirror(*arrow_location) : + BubbleBorder::horizontal_mirror(*arrow_location); + + // Change the arrow and get the new bounds. + bubble_border_->set_arrow_location(*arrow_location); + *window_bounds = bubble_border_->GetBounds(position_relative_to, + local_contents_size); + gfx::Insets new_offscreen_insets; + // If there is more of the window offscreen, we'll keep the old arrow. + if (ComputeOffScreenInsets(monitor_bounds, *window_bounds, + &new_offscreen_insets) && + GetInsetsLength(new_offscreen_insets, vertical) >= + GetInsetsLength(offscreen_insets, vertical)) { + *arrow_location = original_arrow_location; + bubble_border_->set_arrow_location(*arrow_location); + *window_bounds = bubble_border_->GetBounds(position_relative_to, + local_contents_size); + } + } +} + +// static +bool BorderContents::ComputeOffScreenInsets(const gfx::Rect& monitor_bounds, + const gfx::Rect& window_bounds, + gfx::Insets* offscreen_insets) { + if (monitor_bounds.Contains(window_bounds)) + return false; + + if (!offscreen_insets) + return true; + + int top = 0; + int left = 0; + int bottom = 0; + int right = 0; + + if (window_bounds.y() < monitor_bounds.y()) + top = monitor_bounds.y() - window_bounds.y(); + if (window_bounds.x() < monitor_bounds.x()) + left = monitor_bounds.x() - window_bounds.x(); + if (window_bounds.bottom() > monitor_bounds.bottom()) + bottom = window_bounds.bottom() - monitor_bounds.bottom(); + if (window_bounds.right() > monitor_bounds.right()) + right = window_bounds.right() - monitor_bounds.right(); + + offscreen_insets->Set(top, left, bottom, right); + return true; +} + +// static +int BorderContents::GetInsetsLength(const gfx::Insets& insets, bool vertical) { + return vertical ? insets.height() : insets.width(); +} + +#if defined(OS_WIN) +// BorderWidget --------------------------------------------------------------- + +BorderWidget::BorderWidget() : border_contents_(NULL) { + set_window_style(WS_POPUP); + set_window_ex_style(WS_EX_TOOLWINDOW | WS_EX_LAYERED); +} + +void BorderWidget::Init(BorderContents* border_contents, HWND owner) { + DCHECK(!border_contents_); + border_contents_ = border_contents; + border_contents_->Init(); + WidgetWin::Init(owner, gfx::Rect()); + SetContentsView(border_contents_); + SetWindowPos(owner, 0, 0, 0, 0, + SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE | SWP_NOREDRAW); +} + +gfx::Rect BorderWidget::SizeAndGetBounds( + const gfx::Rect& position_relative_to, + BubbleBorder::ArrowLocation arrow_location, + const gfx::Size& contents_size) { + // Ask the border view to calculate our bounds (and our contents'). + gfx::Rect contents_bounds; + gfx::Rect window_bounds; + border_contents_->SizeAndGetBounds(position_relative_to, arrow_location, + false, contents_size, &contents_bounds, + &window_bounds); + SetBounds(window_bounds); + + // Return |contents_bounds| in screen coordinates. + contents_bounds.Offset(window_bounds.origin()); + return contents_bounds; +} + +LRESULT BorderWidget::OnMouseActivate(HWND window, + UINT hit_test, + UINT mouse_message) { + // Never activate. + return MA_NOACTIVATE; +} +#endif + +// InfoBubble ----------------------------------------------------------------- + +// static +InfoBubble* InfoBubble::Show(views::Widget* parent, + const gfx::Rect& position_relative_to, + BubbleBorder::ArrowLocation arrow_location, + views::View* contents, + InfoBubbleDelegate* delegate) { + InfoBubble* window = new InfoBubble; + window->Init(parent, position_relative_to, arrow_location, + contents, delegate); + return window; +} + +#if defined(OS_CHROMEOS) +// static +InfoBubble* InfoBubble::ShowFocusless( + views::Widget* parent, + const gfx::Rect& position_relative_to, + BubbleBorder::ArrowLocation arrow_location, + views::View* contents, + InfoBubbleDelegate* delegate) { + InfoBubble* window = new InfoBubble(views::WidgetGtk::TYPE_POPUP); + window->Init(parent, position_relative_to, arrow_location, + contents, delegate); + return window; +} +#endif + +void InfoBubble::Close() { + if (show_status_ != kOpen) + return; + + show_status_ = kClosing; + + if (fade_away_on_close_) + FadeOut(); + else + DoClose(false); +} + +void InfoBubble::AnimationEnded(const Animation* animation) { + if (static_cast<int>(animation_->GetCurrentValue()) == 0) { + // When fading out we just need to close the bubble at the end + DoClose(false); + } else { +#if defined(OS_WIN) + // When fading in we need to remove the layered window style flag, since + // that style prevents some bubble content from working properly. + SetWindowLong(GWL_EXSTYLE, GetWindowLong(GWL_EXSTYLE) & ~WS_EX_LAYERED); +#endif + } +} + +void InfoBubble::AnimationProgressed(const Animation* animation) { +#if defined(OS_WIN) + // Set the opacity for the main contents window. + unsigned char opacity = static_cast<unsigned char>( + animation_->GetCurrentValue() * 255); + SetLayeredWindowAttributes(GetNativeView(), 0, + static_cast<byte>(opacity), LWA_ALPHA); + contents_->SchedulePaint(); + + // Also fade in/out the bubble border window. + border_->SetOpacity(opacity); + border_->border_contents()->SchedulePaint(); +#else + NOTIMPLEMENTED(); +#endif +} + +InfoBubble::InfoBubble() + : +#if defined(OS_LINUX) + WidgetGtk(TYPE_WINDOW), + border_contents_(NULL), +#elif defined(OS_WIN) + border_(NULL), +#endif + delegate_(NULL), + show_status_(kOpen), + fade_away_on_close_(false), + arrow_location_(BubbleBorder::NONE), + contents_(NULL) { +} + +#if defined(OS_CHROMEOS) +InfoBubble::InfoBubble(views::WidgetGtk::Type type) + : WidgetGtk(type), + border_contents_(NULL), + delegate_(NULL), + show_status_(kOpen), + fade_away_on_close_(false), + arrow_location_(BubbleBorder::NONE), + contents_(NULL) { +} +#endif + +InfoBubble::~InfoBubble() { +} + +void InfoBubble::Init(views::Widget* parent, + const gfx::Rect& position_relative_to, + BubbleBorder::ArrowLocation arrow_location, + views::View* contents, + InfoBubbleDelegate* delegate) { + delegate_ = delegate; + position_relative_to_ = position_relative_to; + arrow_location_ = arrow_location; + contents_ = contents; + + // Create the main window. +#if defined(OS_WIN) + views::Window* parent_window = parent->GetWindow(); + if (parent_window) + parent_window->DisableInactiveRendering(); + set_window_style(WS_POPUP | WS_CLIPCHILDREN); + int extended_style = WS_EX_TOOLWINDOW; + // During FadeIn we need to turn on the layered window style to deal with + // transparency. This flag needs to be reset after fading in is complete. + bool fade_in = delegate_ && delegate_->FadeInOnShow(); + if (fade_in) + extended_style |= WS_EX_LAYERED; + set_window_ex_style(extended_style); + + DCHECK(!border_); + border_ = new BorderWidget(); + + if (fade_in) { + border_->SetOpacity(0); + SetOpacity(0); + } + + border_->Init(CreateBorderContents(), parent->GetNativeView()); + + // We make the BorderWidget the owner of the InfoBubble HWND, so that the + // latter is displayed on top of the former. + WidgetWin::Init(border_->GetNativeView(), gfx::Rect()); + + SetWindowText(GetNativeView(), delegate_->accessible_name().c_str()); +#elif defined(OS_LINUX) + MakeTransparent(); + make_transient_to_parent(); + WidgetGtk::InitWithWidget(parent, gfx::Rect()); +#if defined(OS_CHROMEOS) + chromeos::WmIpc::instance()->SetWindowType( + GetNativeView(), + chromeos::WM_IPC_WINDOW_CHROME_INFO_BUBBLE, + NULL); +#endif +#endif + + // Create a View to hold the contents of the main window. + views::View* contents_view = new views::View; + // We add |contents_view| to ourselves before the AddChildView() call below so + // that when |contents| gets added, it will already have a widget, and thus + // any NativeButtons it creates in ViewHierarchyChanged() will be functional + // (e.g. calling SetChecked() on checkboxes is safe). + SetContentsView(contents_view); + // Adding |contents| as a child has to be done before we call + // contents->GetPreferredSize() below, since some supplied views don't + // actually initialize themselves until they're added to a hierarchy. + contents_view->AddChildView(contents); + + // Calculate and set the bounds for all windows and views. + gfx::Rect window_bounds; + +#if defined(OS_WIN) + // Initialize and position the border window. + window_bounds = border_->SizeAndGetBounds(position_relative_to, + arrow_location, + contents->GetPreferredSize()); + + // Make |contents| take up the entire contents view. + contents_view->SetLayoutManager(new views::FillLayout); + + // Paint the background color behind the contents. + contents_view->set_background( + views::Background::CreateSolidBackground(kBackgroundColor)); +#else + // Create a view to paint the border and background. + border_contents_ = CreateBorderContents(); + border_contents_->Init(); + gfx::Rect contents_bounds; + border_contents_->SizeAndGetBounds(position_relative_to, + arrow_location, false, contents->GetPreferredSize(), + &contents_bounds, &window_bounds); + // This new view must be added before |contents| so it will paint under it. + contents_view->AddChildView(0, border_contents_); + + // |contents_view| has no layout manager, so we have to explicitly position + // its children. + border_contents_->SetBounds(gfx::Rect(gfx::Point(), window_bounds.size())); + contents->SetBounds(contents_bounds); +#endif + SetBounds(window_bounds); + + // Register the Escape accelerator for closing. + GetFocusManager()->RegisterAccelerator( + views::Accelerator(app::VKEY_ESCAPE, false, false, false), this); + + // Done creating the bubble. + NotificationService::current()->Notify(NotificationType::INFO_BUBBLE_CREATED, + Source<InfoBubble>(this), + NotificationService::NoDetails()); + + // Show the window. +#if defined(OS_WIN) + border_->ShowWindow(SW_SHOW); + ShowWindow(SW_SHOW); + if (fade_in) + FadeIn(); +#elif defined(OS_LINUX) + views::WidgetGtk::Show(); +#endif +} + +BorderContents* InfoBubble::CreateBorderContents() { + return new BorderContents(); +} + +void InfoBubble::SizeToContents() { + gfx::Rect window_bounds; + +#if defined(OS_WIN) + // Initialize and position the border window. + window_bounds = border_->SizeAndGetBounds(position_relative_to_, + arrow_location_, + contents_->GetPreferredSize()); +#else + gfx::Rect contents_bounds; + border_contents_->SizeAndGetBounds(position_relative_to_, + arrow_location_, false, contents_->GetPreferredSize(), + &contents_bounds, &window_bounds); + // |contents_view| has no layout manager, so we have to explicitly position + // its children. + border_contents_->SetBounds(gfx::Rect(gfx::Point(), window_bounds.size())); + contents_->SetBounds(contents_bounds); +#endif + SetBounds(window_bounds); +} + +#if defined(OS_WIN) +void InfoBubble::OnActivate(UINT action, BOOL minimized, HWND window) { + // The popup should close when it is deactivated. + if (action == WA_INACTIVE) { + Close(); + } else if (action == WA_ACTIVE) { + DCHECK_GT(GetRootView()->GetChildViewCount(), 0); + GetRootView()->GetChildViewAt(0)->RequestFocus(); + } +} +#elif defined(OS_LINUX) +void InfoBubble::IsActiveChanged() { + if (!IsActive()) + Close(); +} +#endif + +void InfoBubble::DoClose(bool closed_by_escape) { + if (show_status_ == kClosed) + return; + + GetFocusManager()->UnregisterAccelerator( + views::Accelerator(app::VKEY_ESCAPE, false, false, false), this); + if (delegate_) + delegate_->InfoBubbleClosing(this, closed_by_escape); + show_status_ = kClosed; +#if defined(OS_WIN) + border_->Close(); + WidgetWin::Close(); +#elif defined(OS_LINUX) + WidgetGtk::Close(); +#endif +} + +void InfoBubble::FadeIn() { + Fade(true); // |fade_in|. +} + +void InfoBubble::FadeOut() { +#if defined(OS_WIN) + // The contents window cannot have the layered flag on by default, since its + // content doesn't always work inside a layered window, but when animating it + // is ok to set that style on the window for the purpose of fading it out. + SetWindowLong(GWL_EXSTYLE, GetWindowLong(GWL_EXSTYLE) | WS_EX_LAYERED); + // This must be the very next call, otherwise we can get flicker on close. + SetLayeredWindowAttributes(GetNativeView(), 0, + static_cast<byte>(255), LWA_ALPHA); +#endif + + Fade(false); // |fade_in|. +} + +void InfoBubble::Fade(bool fade_in) { + animation_.reset(new SlideAnimation(this)); + animation_->SetSlideDuration(kHideFadeDurationMS); + animation_->SetTweenType(Tween::LINEAR); + + animation_->Reset(fade_in ? 0.0 : 1.0); + if (fade_in) + animation_->Show(); + else + animation_->Hide(); +} + +bool InfoBubble::AcceleratorPressed(const views::Accelerator& accelerator) { + if (!delegate_ || delegate_->CloseOnEscape()) { + DoClose(true); + return true; + } + return false; +} |