// 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 "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 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(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( animation_->GetCurrentValue() * 255); SetLayeredWindowAttributes(GetNativeView(), 0, static_cast(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) { } #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) { } #endif 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(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(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; }