blob: 53827b7ba373a8d7b2c2f0311d6c5648f2668f22 [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 "chrome/browser/ui/views/exclusive_access_bubble_views.h"
#include <utility>
#include "base/i18n/case_conversion.h"
#include "base/location.h"
#include "base/macros.h"
#include "base/single_thread_task_runner.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/thread_task_runner_handle.h"
#include "build/build_config.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/chrome_notification_types.h"
#include "chrome/browser/ui/exclusive_access/exclusive_access_manager.h"
#include "chrome/browser/ui/exclusive_access/fullscreen_controller.h"
#include "chrome/browser/ui/views/exclusive_access_bubble_views_context.h"
#include "chrome/browser/ui/views/frame/immersive_mode_controller.h"
#include "chrome/browser/ui/views/frame/top_container_view.h"
#include "chrome/browser/ui/views/subtle_notification_view.h"
#include "chrome/grit/generated_resources.h"
#include "content/public/browser/notification_service.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/events/keycodes/keyboard_codes.h"
#include "ui/gfx/animation/slide_animation.h"
#include "ui/gfx/canvas.h"
#include "ui/strings/grit/ui_strings.h"
#include "ui/views/bubble/bubble_border.h"
#include "ui/views/controls/link.h"
#include "ui/views/view.h"
#include "ui/views/widget/widget.h"
#include "url/gurl.h"
#if defined(OS_WIN)
#include "ui/base/l10n/l10n_util_win.h"
#endif
ExclusiveAccessBubbleViews::ExclusiveAccessBubbleViews(
ExclusiveAccessBubbleViewsContext* context,
const GURL& url,
ExclusiveAccessBubbleType bubble_type,
ExclusiveAccessBubbleHideCallback bubble_first_hide_callback)
: ExclusiveAccessBubble(context->GetExclusiveAccessManager(),
url,
bubble_type),
bubble_view_context_(context),
popup_(nullptr),
bubble_first_hide_callback_(std::move(bubble_first_hide_callback)),
animation_(new gfx::SlideAnimation(this)) {
// Create the contents view.
ui::Accelerator accelerator(ui::VKEY_UNKNOWN, ui::EF_NONE);
bool got_accelerator =
bubble_view_context_->GetAcceleratorProvider()
->GetAcceleratorForCommandId(IDC_FULLSCREEN, &accelerator);
DCHECK(got_accelerator);
view_ = new SubtleNotificationView();
browser_fullscreen_exit_accelerator_ = accelerator.GetShortcutText();
UpdateViewContent(bubble_type_);
// Initialize the popup.
popup_ = SubtleNotificationView::CreatePopupWidget(
bubble_view_context_->GetBubbleParentView(), view_);
gfx::Size size = GetPopupRect(true).size();
// Bounds are in screen coordinates.
popup_->SetBounds(GetPopupRect(false));
view_->SetBounds(0, 0, size.width(), size.height());
popup_->AddObserver(this);
registrar_.Add(this, chrome::NOTIFICATION_FULLSCREEN_CHANGED,
content::Source<FullscreenController>(
bubble_view_context_->GetExclusiveAccessManager()
->fullscreen_controller()));
UpdateMouseWatcher();
}
ExclusiveAccessBubbleViews::~ExclusiveAccessBubbleViews() {
RunHideCallbackIfNeeded(ExclusiveAccessBubbleHideReason::kInterrupted);
popup_->RemoveObserver(this);
// This is tricky. We may be in an ATL message handler stack, in which case
// the popup cannot be deleted yet. We also can't set the popup's ownership
// model to NATIVE_WIDGET_OWNS_WIDGET because if the user closed the last tab
// while in fullscreen mode, Windows has already destroyed the popup HWND by
// the time we get here, and thus either the popup will already have been
// deleted (if we set this in our constructor) or the popup will never get
// another OnFinalMessage() call (if not, as currently). So instead, we tell
// the popup to synchronously hide, and then asynchronously close and delete
// itself.
popup_->Close();
base::ThreadTaskRunnerHandle::Get()->DeleteSoon(FROM_HERE, popup_);
}
void ExclusiveAccessBubbleViews::UpdateContent(
const GURL& url,
ExclusiveAccessBubbleType bubble_type,
ExclusiveAccessBubbleHideCallback bubble_first_hide_callback,
bool force_update) {
DCHECK_NE(EXCLUSIVE_ACCESS_BUBBLE_TYPE_NONE, bubble_type);
if (bubble_type_ == bubble_type && url_ == url && !force_update)
return;
// Bubble maybe be re-used after timeout.
RunHideCallbackIfNeeded(ExclusiveAccessBubbleHideReason::kInterrupted);
bubble_first_hide_callback_ = std::move(bubble_first_hide_callback);
url_ = url;
bubble_type_ = bubble_type;
UpdateViewContent(bubble_type_);
gfx::Size size = GetPopupRect(true).size();
view_->SetSize(size);
popup_->SetBounds(GetPopupRect(false));
Show();
// Stop watching the mouse even if UpdateMouseWatcher() will start watching
// it again so that the popup with the new content is visible for at least
// |kInitialDelayMs|.
StopWatchingMouse();
UpdateMouseWatcher();
}
void ExclusiveAccessBubbleViews::RepositionIfVisible() {
if (popup_->IsVisible())
UpdateBounds();
}
void ExclusiveAccessBubbleViews::HideImmediately() {
if (!popup_->IsVisible())
return;
RunHideCallbackIfNeeded(ExclusiveAccessBubbleHideReason::kInterrupted);
animation_->SetSlideDuration(kQuickSlideOutDurationMs);
animation_->Hide();
}
views::View* ExclusiveAccessBubbleViews::GetView() {
return view_;
}
void ExclusiveAccessBubbleViews::UpdateMouseWatcher() {
bool should_watch_mouse = popup_->IsVisible() || CanTriggerOnMouse();
if (should_watch_mouse == IsWatchingMouse())
return;
if (should_watch_mouse)
StartWatchingMouse();
else
StopWatchingMouse();
}
void ExclusiveAccessBubbleViews::UpdateBounds() {
gfx::Rect popup_rect(GetPopupRect(false));
if (!popup_rect.IsEmpty()) {
popup_->SetBounds(popup_rect);
view_->SetY(popup_rect.height() - view_->height());
}
}
void ExclusiveAccessBubbleViews::UpdateViewContent(
ExclusiveAccessBubbleType bubble_type) {
DCHECK_NE(EXCLUSIVE_ACCESS_BUBBLE_TYPE_NONE, bubble_type);
base::string16 accelerator;
if (bubble_type ==
EXCLUSIVE_ACCESS_BUBBLE_TYPE_BROWSER_FULLSCREEN_EXIT_INSTRUCTION ||
bubble_type ==
EXCLUSIVE_ACCESS_BUBBLE_TYPE_EXTENSION_FULLSCREEN_EXIT_INSTRUCTION) {
accelerator = browser_fullscreen_exit_accelerator_;
} else {
accelerator = l10n_util::GetStringUTF16(IDS_APP_ESC_KEY);
}
#if defined(OS_MACOSX)
// Mac keyboards use lowercase for everything except function keys, which are
// typically reserved for system use. Since |accelerator| is placed in a box
// to make it look like a keyboard key it looks weird to not follow suit.
accelerator = base::i18n::ToLower(accelerator);
#endif
view_->UpdateContent(GetInstructionText(accelerator));
}
views::View* ExclusiveAccessBubbleViews::GetBrowserRootView() const {
return bubble_view_context_->GetBubbleAssociatedWidget()->GetRootView();
}
void ExclusiveAccessBubbleViews::AnimationProgressed(
const gfx::Animation* animation) {
float opacity = static_cast<float>(animation_->CurrentValueBetween(0.0, 1.0));
if (opacity == 0) {
popup_->Hide();
} else {
popup_->Show();
popup_->SetOpacity(opacity);
}
}
void ExclusiveAccessBubbleViews::AnimationEnded(
const gfx::Animation* animation) {
if (animation_->IsShowing())
GetView()->NotifyAccessibilityEvent(ax::mojom::Event::kAlert, true);
AnimationProgressed(animation);
}
gfx::Rect ExclusiveAccessBubbleViews::GetPopupRect(
bool ignore_animation_state) const {
gfx::Size size(view_->GetPreferredSize());
gfx::Rect widget_bounds = bubble_view_context_->GetClientAreaBoundsInScreen();
int x = widget_bounds.x() + (widget_bounds.width() - size.width()) / 2;
int top_container_bottom = widget_bounds.y();
if (bubble_view_context_->IsImmersiveModeEnabled()) {
// Skip querying the top container height in non-immersive fullscreen
// because:
// - The top container height is always zero in non-immersive fullscreen.
// - Querying the top container height may return the height before entering
// fullscreen because layout is disabled while entering fullscreen.
// A visual glitch due to the delayed layout is avoided in immersive
// fullscreen because entering fullscreen starts with the top container
// revealed. When revealed, the top container has the same height as before
// entering fullscreen.
top_container_bottom =
bubble_view_context_->GetTopContainerBoundsInScreen().bottom();
}
// |desired_top| is the top of the bubble area including the shadow.
int desired_top = kSimplifiedPopupTopPx - view_->border()->GetInsets().top();
int y = top_container_bottom + desired_top;
return gfx::Rect(gfx::Point(x, y), size);
}
gfx::Point ExclusiveAccessBubbleViews::GetCursorScreenPoint() {
return bubble_view_context_->GetCursorPointInParent();
}
bool ExclusiveAccessBubbleViews::WindowContainsPoint(gfx::Point pos) {
return GetBrowserRootView()->HitTestPoint(pos);
}
bool ExclusiveAccessBubbleViews::IsWindowActive() {
return bubble_view_context_->GetBubbleAssociatedWidget()->IsActive();
}
void ExclusiveAccessBubbleViews::Hide() {
// This function is guarded by the |ExclusiveAccessBubble::hide_timeout_|
// timer, so the bubble has been displayed for at least
// |ExclusiveAccessBubble::kInitialDelayMs|.
DCHECK(!IsHideTimeoutRunning());
RunHideCallbackIfNeeded(ExclusiveAccessBubbleHideReason::kTimeout);
animation_->SetSlideDuration(kSlideOutDurationMs);
animation_->Hide();
}
void ExclusiveAccessBubbleViews::Show() {
if (animation_->IsShowing())
return;
animation_->SetSlideDuration(kSlideInDurationMs);
animation_->Show();
}
bool ExclusiveAccessBubbleViews::IsAnimating() {
return animation_->is_animating();
}
bool ExclusiveAccessBubbleViews::CanTriggerOnMouse() const {
return bubble_view_context_->CanTriggerOnMouse();
}
void ExclusiveAccessBubbleViews::Observe(
int type,
const content::NotificationSource& source,
const content::NotificationDetails& details) {
DCHECK_EQ(chrome::NOTIFICATION_FULLSCREEN_CHANGED, type);
UpdateMouseWatcher();
}
void ExclusiveAccessBubbleViews::OnWidgetDestroyed(views::Widget* widget) {
// Although SubtleNotificationView uses WIDGET_OWNS_NATIVE_WIDGET, a close can
// originate from the OS or some Chrome shutdown codepaths that bypass the
// destructor.
views::Widget* popup_on_stack = popup_;
DCHECK(popup_on_stack->HasObserver(this));
// Get ourselves destroyed. Calling ExitExclusiveAccess() won't work because
// the parent window might be destroyed as well, so asking it to exit
// fullscreen would be a bad idea.
bubble_view_context_->DestroyAnyExclusiveAccessBubble();
// Note: |this| is destroyed on the line above. Check that the destructor was
// invoked. This is safe to do since |popup_| is deleted via a posted task.
DCHECK(!popup_on_stack->HasObserver(this));
}
void ExclusiveAccessBubbleViews::OnWidgetVisibilityChanged(
views::Widget* widget,
bool visible) {
UpdateMouseWatcher();
}
void ExclusiveAccessBubbleViews::RunHideCallbackIfNeeded(
ExclusiveAccessBubbleHideReason reason) {
if (bubble_first_hide_callback_)
std::move(bubble_first_hide_callback_).Run(reason);
}