summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--chrome/browser/ui/BUILD.gn1
-rw-r--r--chrome/browser/ui/DEPS1
-rw-r--r--chrome/browser/ui/browser.cc7
-rw-r--r--chrome/browser/ui/browser.h6
-rw-r--r--chrome/browser/ui/chrome_bubble_manager.cc50
-rw-r--r--chrome/browser/ui/chrome_bubble_manager.h40
-rw-r--r--chrome/browser/ui/cocoa/browser_window_controller_private.mm2
-rw-r--r--chrome/chrome_browser_ui.gypi3
-rw-r--r--components/BUILD.gn2
-rw-r--r--components/OWNERS5
-rw-r--r--components/bubble.gypi27
-rw-r--r--components/bubble/BUILD.gn30
-rw-r--r--components/bubble/OWNERS4
-rw-r--r--components/bubble/bubble_close_reason.h41
-rw-r--r--components/bubble/bubble_controller.cc49
-rw-r--r--components/bubble/bubble_controller.h47
-rw-r--r--components/bubble/bubble_delegate.cc13
-rw-r--r--components/bubble/bubble_delegate.h34
-rw-r--r--components/bubble/bubble_manager.cc106
-rw-r--r--components/bubble/bubble_manager.h69
-rw-r--r--components/bubble/bubble_manager_unittest.cc347
-rw-r--r--components/bubble/bubble_ui.h24
-rw-r--r--components/components.gyp1
-rw-r--r--components/components_tests.gyp5
24 files changed, 914 insertions, 0 deletions
diff --git a/chrome/browser/ui/BUILD.gn b/chrome/browser/ui/BUILD.gn
index e7b111a..3e05424 100644
--- a/chrome/browser/ui/BUILD.gn
+++ b/chrome/browser/ui/BUILD.gn
@@ -91,6 +91,7 @@ source_set("ui") {
"//chrome/common/net",
"//chrome/installer/util",
"//components/autofill/content/browser:risk_proto",
+ "//components/bubble:bubble",
"//components/power",
"//components/suggestions/proto",
"//components/url_formatter",
diff --git a/chrome/browser/ui/DEPS b/chrome/browser/ui/DEPS
index 1e7e329..aff1249 100644
--- a/chrome/browser/ui/DEPS
+++ b/chrome/browser/ui/DEPS
@@ -1,5 +1,6 @@
include_rules = [
"+components/auto_login_parser",
+ "+components/bubble",
"+components/favicon/core",
"+components/toolbar",
"+components/url_formatter",
diff --git a/chrome/browser/ui/browser.cc b/chrome/browser/ui/browser.cc
index 72b0ce3..5ad781c 100644
--- a/chrome/browser/ui/browser.cc
+++ b/chrome/browser/ui/browser.cc
@@ -471,6 +471,7 @@ Browser::~Browser() {
// The tab strip should not have any tabs at this point.
DCHECK(tab_strip_model_->empty());
tab_strip_model_->RemoveObserver(this);
+ bubble_manager_.reset();
// Destroy the BrowserCommandController before removing the browser, so that
// it doesn't act on any notifications that are sent as a result of removing
@@ -549,6 +550,12 @@ Browser::~Browser() {
///////////////////////////////////////////////////////////////////////////////
// Getters & Setters
+ChromeBubbleManager* Browser::GetBubbleManager() {
+ if (!bubble_manager_)
+ bubble_manager_.reset(new ChromeBubbleManager(tab_strip_model_.get()));
+ return bubble_manager_.get();
+}
+
FindBarController* Browser::GetFindBarController() {
if (!find_bar_controller_.get()) {
FindBar* find_bar = window_->CreateFindBar();
diff --git a/chrome/browser/ui/browser.h b/chrome/browser/ui/browser.h
index 362d85a..56add13 100644
--- a/chrome/browser/ui/browser.h
+++ b/chrome/browser/ui/browser.h
@@ -23,6 +23,7 @@
#include "chrome/browser/ui/bookmarks/bookmark_bar.h"
#include "chrome/browser/ui/bookmarks/bookmark_tab_helper_delegate.h"
#include "chrome/browser/ui/browser_navigator.h"
+#include "chrome/browser/ui/chrome_bubble_manager.h"
#include "chrome/browser/ui/chrome_web_modal_dialog_manager_delegate.h"
#include "chrome/browser/ui/exclusive_access/exclusive_access_manager.h"
#include "chrome/browser/ui/host_desktop.h"
@@ -283,6 +284,9 @@ class Browser : public TabStripModelObserver,
return hosted_app_controller_.get();
}
+ // Will lazy create the bubble manager.
+ ChromeBubbleManager* GetBubbleManager();
+
// Get the FindBarController for this browser, creating it if it does not
// yet exist.
FindBarController* GetFindBarController();
@@ -924,6 +928,8 @@ class Browser : public TabStripModelObserver,
scoped_ptr<chrome::UnloadController> unload_controller_;
scoped_ptr<chrome::FastUnloadController> fast_unload_controller_;
+ scoped_ptr<ChromeBubbleManager> bubble_manager_;
+
// The Find Bar. This may be NULL if there is no Find Bar, and if it is
// non-NULL, it may or may not be visible.
scoped_ptr<FindBarController> find_bar_controller_;
diff --git a/chrome/browser/ui/chrome_bubble_manager.cc b/chrome/browser/ui/chrome_bubble_manager.cc
new file mode 100644
index 0000000..60d3f42
--- /dev/null
+++ b/chrome/browser/ui/chrome_bubble_manager.cc
@@ -0,0 +1,50 @@
+// Copyright 2015 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/chrome_bubble_manager.h"
+
+#include "chrome/browser/ui/tabs/tab_strip_model.h"
+#include "content/public/browser/browser_thread.h"
+#include "content/public/browser/web_contents.h"
+
+ChromeBubbleManager::ChromeBubbleManager(TabStripModel* tab_strip_model)
+ : tab_strip_model_(tab_strip_model) {
+ DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+ DCHECK(tab_strip_model_);
+ tab_strip_model_->AddObserver(this);
+}
+
+ChromeBubbleManager::~ChromeBubbleManager() {
+ tab_strip_model_->RemoveObserver(this);
+}
+
+void ChromeBubbleManager::TabDetachedAt(content::WebContents* contents,
+ int index) {
+ CloseAllBubbles(BUBBLE_CLOSE_TABDETACHED);
+ // Any bubble that didn't close should update its anchor position.
+ UpdateAllBubbleAnchors();
+}
+
+void ChromeBubbleManager::TabDeactivated(content::WebContents* contents) {
+ CloseAllBubbles(BUBBLE_CLOSE_TABSWITCHED);
+}
+
+void ChromeBubbleManager::ActiveTabChanged(content::WebContents* old_contents,
+ content::WebContents* new_contents,
+ int index,
+ int reason) {
+ Observe(new_contents);
+}
+
+void ChromeBubbleManager::DidToggleFullscreenModeForTab(
+ bool entered_fullscreen) {
+ CloseAllBubbles(BUBBLE_CLOSE_FULLSCREEN_TOGGLED);
+ // Any bubble that didn't close should update its anchor position.
+ UpdateAllBubbleAnchors();
+}
+
+void ChromeBubbleManager::NavigationEntryCommitted(
+ const content::LoadCommittedDetails& load_details) {
+ CloseAllBubbles(BUBBLE_CLOSE_NAVIGATED);
+}
diff --git a/chrome/browser/ui/chrome_bubble_manager.h b/chrome/browser/ui/chrome_bubble_manager.h
new file mode 100644
index 0000000..2c7dd7e
--- /dev/null
+++ b/chrome/browser/ui/chrome_bubble_manager.h
@@ -0,0 +1,40 @@
+// Copyright 2015 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.
+
+#ifndef CHROME_BROWSER_UI_CHROME_BUBBLE_MANAGER_H_
+#define CHROME_BROWSER_UI_CHROME_BUBBLE_MANAGER_H_
+
+#include "chrome/browser/ui/tabs/tab_strip_model_observer.h"
+#include "components/bubble/bubble_manager.h"
+#include "content/public/browser/web_contents_observer.h"
+
+class TabStripModel;
+
+class ChromeBubbleManager : public BubbleManager,
+ public TabStripModelObserver,
+ public content::WebContentsObserver {
+ public:
+ explicit ChromeBubbleManager(TabStripModel* tab_strip_model);
+ ~ChromeBubbleManager() override;
+
+ // TabStripModelObserver:
+ void TabDetachedAt(content::WebContents* contents, int index) override;
+ void TabDeactivated(content::WebContents* contents) override;
+ void ActiveTabChanged(content::WebContents* old_contents,
+ content::WebContents* new_contents,
+ int index,
+ int reason) override;
+
+ // content::WebContentsObserver:
+ void DidToggleFullscreenModeForTab(bool entered_fullscreen) override;
+ void NavigationEntryCommitted(
+ const content::LoadCommittedDetails& load_details) override;
+
+ private:
+ TabStripModel* tab_strip_model_;
+
+ DISALLOW_COPY_AND_ASSIGN(ChromeBubbleManager);
+};
+
+#endif // CHROME_BROWSER_UI_CHROME_BUBBLE_MANAGER_H_
diff --git a/chrome/browser/ui/cocoa/browser_window_controller_private.mm b/chrome/browser/ui/cocoa/browser_window_controller_private.mm
index d680763..6cf4445 100644
--- a/chrome/browser/ui/cocoa/browser_window_controller_private.mm
+++ b/chrome/browser/ui/cocoa/browser_window_controller_private.mm
@@ -235,6 +235,8 @@ willPositionSheet:(NSWindow*)sheet
PermissionBubbleManager* manager = [self permissionBubbleManager];
if (manager)
manager->UpdateAnchorPosition();
+
+ browser_->GetBubbleManager()->UpdateAllBubbleAnchors();
}
- (void)applyTabStripLayout:(const chrome::TabStripLayout&)layout {
diff --git a/chrome/chrome_browser_ui.gypi b/chrome/chrome_browser_ui.gypi
index b06e056..6e82245 100644
--- a/chrome/chrome_browser_ui.gypi
+++ b/chrome/chrome_browser_ui.gypi
@@ -1541,6 +1541,8 @@
'browser/ui/browser_view_prefs.h',
'browser/ui/browser_window_state.cc',
'browser/ui/browser_window_state.h',
+ 'browser/ui/chrome_bubble_manager.cc',
+ 'browser/ui/chrome_bubble_manager.h',
'browser/ui/chrome_pages.cc',
'browser/ui/chrome_pages.h',
'browser/ui/chrome_style.cc',
@@ -3183,6 +3185,7 @@
}],
['OS!="android" and OS!="ios"', {
'dependencies': [
+ '../components/components.gyp:bubble',
'../components/components.gyp:feedback_proto',
'../device/bluetooth/bluetooth.gyp:device_bluetooth',
'../third_party/libusb/libusb.gyp:libusb',
diff --git a/components/BUILD.gn b/components/BUILD.gn
index fc26388..07ed6c7 100644
--- a/components/BUILD.gn
+++ b/components/BUILD.gn
@@ -23,6 +23,7 @@ group("all_components") {
"//components/bookmarks/common",
"//components/bookmarks/managed",
"//components/bookmarks/test",
+ "//components/bubble",
"//components/captive_portal",
"//components/cdm/browser",
"//components/cdm/common",
@@ -306,6 +307,7 @@ test("components_unittests") {
"//components/autofill/core/common:unit_tests",
"//components/bookmarks/browser:unit_tests",
"//components/bookmarks/managed:unit_tests",
+ "//components/bubble:unit_tests",
"//components/captive_portal:unit_tests",
"//components/cloud_devices/common:unit_tests",
"//components/component_updater:unit_tests",
diff --git a/components/OWNERS b/components/OWNERS
index 754e07c..cfa64621 100644
--- a/components/OWNERS
+++ b/components/OWNERS
@@ -23,6 +23,11 @@ per-file browsing_data.gypi=mkwst@chromium.org
per-file browsing_data.gypi=bauerb@chromium.org
per-file browsing_data.gypi=michaeln@chromium.org
+per-file bubble.gypi=groby@chromium.org
+per-file bubble.gypi=hcarmona@chromium.org
+per-file bubble.gypi=msw@chromium.org
+per-file bubble.gypi=rouslan@chromium.org
+
per-file cdm.gypi=ddorwin@chromium.org
per-file cdm.gypi=xhwang@chromium.org
diff --git a/components/bubble.gypi b/components/bubble.gypi
new file mode 100644
index 0000000..8092459
--- /dev/null
+++ b/components/bubble.gypi
@@ -0,0 +1,27 @@
+# Copyright 2015 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.
+
+{
+ 'targets': [
+ {
+ # GN version: //components/bubble
+ 'target_name': 'bubble',
+ 'type': 'static_library',
+ 'include_dirs': [
+ '..',
+ ],
+ 'sources': [
+ # Note: sources list duplicated in GN build.
+ 'bubble/bubble_close_reason.h',
+ 'bubble/bubble_controller.cc',
+ 'bubble/bubble_controller.h',
+ 'bubble/bubble_delegate.cc',
+ 'bubble/bubble_delegate.h',
+ 'bubble/bubble_manager.cc',
+ 'bubble/bubble_manager.h',
+ 'bubble/bubble_ui.h',
+ ],
+ },
+ ],
+}
diff --git a/components/bubble/BUILD.gn b/components/bubble/BUILD.gn
new file mode 100644
index 0000000..cd9afbd
--- /dev/null
+++ b/components/bubble/BUILD.gn
@@ -0,0 +1,30 @@
+# Copyright 2015 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.
+
+source_set("bubble") {
+ sources = [
+ "bubble_close_reason.h",
+ "bubble_controller.cc",
+ "bubble_controller.h",
+ "bubble_delegate.cc",
+ "bubble_delegate.h",
+ "bubble_manager.cc",
+ "bubble_manager.h",
+ "bubble_ui.h",
+ ]
+}
+
+source_set("unit_tests") {
+ testonly = true
+
+ sources = [
+ "bubble_manager_unittest.cc",
+ ]
+
+ deps = [
+ ":bubble",
+ "//testing/gmock",
+ "//testing/gtest",
+ ]
+}
diff --git a/components/bubble/OWNERS b/components/bubble/OWNERS
new file mode 100644
index 0000000..7a7980c
--- /dev/null
+++ b/components/bubble/OWNERS
@@ -0,0 +1,4 @@
+groby@chromium.org
+hcarmona@chromium.org
+msw@chromium.org
+rouslan@chromium.org
diff --git a/components/bubble/bubble_close_reason.h b/components/bubble/bubble_close_reason.h
new file mode 100644
index 0000000..bfd84b3
--- /dev/null
+++ b/components/bubble/bubble_close_reason.h
@@ -0,0 +1,41 @@
+// Copyright 2015 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.
+
+#ifndef COMPONENTS_BUBBLE_BUBBLE_CLOSE_REASON_H_
+#define COMPONENTS_BUBBLE_BUBBLE_CLOSE_REASON_H_
+
+// List of reasons why a bubble might close. These correspond to various events
+// from the UI. Not all platforms will receive all events.
+enum BubbleCloseReason {
+ // Bubble was closed without any user interaction.
+ BUBBLE_CLOSE_FOCUS_LOST,
+
+ // User did not interact with the bubble, but changed tab.
+ BUBBLE_CLOSE_TABSWITCHED,
+
+ // User did not interact with the bubble, but detached the tab.
+ BUBBLE_CLOSE_TABDETACHED,
+
+ // User dismissed the bubble. (ESC, close, etc.)
+ BUBBLE_CLOSE_USER_DISMISSED,
+
+ // There has been a navigation event. (Link, URL typed, refresh, etc.)
+ BUBBLE_CLOSE_NAVIGATED,
+
+ // The parent window has entered or exited fullscreen mode. Will also be
+ // called for immersive fullscreen.
+ BUBBLE_CLOSE_FULLSCREEN_TOGGLED,
+
+ // The user selected an affirmative response in the bubble.
+ BUBBLE_CLOSE_ACCEPTED,
+
+ // The user selected a negative response in the bubble.
+ BUBBLE_CLOSE_CANCELED,
+
+ // The bubble WILL be closed regardless of return value for |ShouldClose|.
+ // Ex: The bubble's parent window is being destroyed.
+ BUBBLE_CLOSE_FORCED,
+};
+
+#endif // COMPONENTS_BUBBLE_BUBBLE_CLOSE_REASON_H_
diff --git a/components/bubble/bubble_controller.cc b/components/bubble/bubble_controller.cc
new file mode 100644
index 0000000..877b3f8
--- /dev/null
+++ b/components/bubble/bubble_controller.cc
@@ -0,0 +1,49 @@
+// Copyright 2015 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 "components/bubble/bubble_controller.h"
+
+#include "components/bubble/bubble_delegate.h"
+#include "components/bubble/bubble_manager.h"
+#include "components/bubble/bubble_ui.h"
+
+BubbleController::BubbleController(BubbleManager* manager,
+ scoped_ptr<BubbleDelegate> delegate)
+ : manager_(manager), delegate_(delegate.Pass()) {
+ DCHECK(manager_);
+ DCHECK(delegate_);
+}
+
+BubbleController::~BubbleController() {
+ if (bubble_ui_)
+ ShouldClose(BUBBLE_CLOSE_FORCED);
+}
+
+bool BubbleController::CloseBubble(BubbleCloseReason reason) {
+ return manager_->CloseBubble(this->AsWeakPtr(), reason);
+}
+
+void BubbleController::Show() {
+ DCHECK(!bubble_ui_);
+ bubble_ui_ = delegate_->BuildBubbleUI();
+ DCHECK(bubble_ui_);
+ bubble_ui_->Show();
+ // TODO(hcarmona): log that bubble was shown.
+}
+
+void BubbleController::UpdateAnchorPosition() {
+ DCHECK(bubble_ui_);
+ bubble_ui_->UpdateAnchorPosition();
+}
+
+bool BubbleController::ShouldClose(BubbleCloseReason reason) {
+ DCHECK(bubble_ui_);
+ if (delegate_->ShouldClose(reason) || reason == BUBBLE_CLOSE_FORCED) {
+ bubble_ui_->Close();
+ bubble_ui_.reset();
+ // TODO(hcarmona): log that bubble was hidden.
+ return true;
+ }
+ return false;
+}
diff --git a/components/bubble/bubble_controller.h b/components/bubble/bubble_controller.h
new file mode 100644
index 0000000..bd87d2d
--- /dev/null
+++ b/components/bubble/bubble_controller.h
@@ -0,0 +1,47 @@
+// Copyright 2015 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.
+
+#ifndef COMPONENTS_BUBBLE_BUBBLE_CONTROLLER_H_
+#define COMPONENTS_BUBBLE_BUBBLE_CONTROLLER_H_
+
+#include "base/memory/scoped_ptr.h"
+#include "base/memory/weak_ptr.h"
+#include "components/bubble/bubble_close_reason.h"
+
+class BubbleDelegate;
+class BubbleManager;
+class BubbleUI;
+
+// BubbleController is responsible for the lifetime of the delegate and its UI.
+class BubbleController : public base::SupportsWeakPtr<BubbleController> {
+ public:
+ explicit BubbleController(BubbleManager* manager,
+ scoped_ptr<BubbleDelegate> delegate);
+ virtual ~BubbleController();
+
+ // Calls CloseBubble on the associated BubbleManager.
+ bool CloseBubble(BubbleCloseReason reason);
+
+ private:
+ friend class BubbleManager;
+
+ // Creates and shows the UI for the delegate.
+ void Show();
+
+ // Notifies the bubble UI that it should update its anchor location.
+ // Important when there's a UI change (ex: fullscreen transition).
+ void UpdateAnchorPosition();
+
+ // Cleans up the delegate and its UI if it closed.
+ // Returns true if the bubble was closed.
+ bool ShouldClose(BubbleCloseReason reason);
+
+ BubbleManager* manager_;
+ scoped_ptr<BubbleDelegate> delegate_;
+ scoped_ptr<BubbleUI> bubble_ui_;
+
+ DISALLOW_COPY_AND_ASSIGN(BubbleController);
+};
+
+#endif // COMPONENTS_BUBBLE_BUBBLE_CONTROLLER_H_
diff --git a/components/bubble/bubble_delegate.cc b/components/bubble/bubble_delegate.cc
new file mode 100644
index 0000000..110d93f
--- /dev/null
+++ b/components/bubble/bubble_delegate.cc
@@ -0,0 +1,13 @@
+// Copyright 2015 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 "components/bubble/bubble_delegate.h"
+
+BubbleDelegate::BubbleDelegate() {}
+
+BubbleDelegate::~BubbleDelegate() {}
+
+bool BubbleDelegate::ShouldClose(BubbleCloseReason reason) {
+ return true;
+}
diff --git a/components/bubble/bubble_delegate.h b/components/bubble/bubble_delegate.h
new file mode 100644
index 0000000..ebac50f
--- /dev/null
+++ b/components/bubble/bubble_delegate.h
@@ -0,0 +1,34 @@
+// Copyright 2015 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.
+
+#ifndef COMPONENTS_BUBBLE_BUBBLE_DELEGATE_H_
+#define COMPONENTS_BUBBLE_BUBBLE_DELEGATE_H_
+
+#include "base/memory/scoped_ptr.h"
+#include "components/bubble/bubble_close_reason.h"
+
+class BubbleUI;
+
+// Inherit from this class to define a bubble. A bubble is a small transient UI
+// surface anchored to a parent window. Most bubbles are dismissed when they
+// lose focus.
+class BubbleDelegate {
+ public:
+ BubbleDelegate();
+ virtual ~BubbleDelegate();
+
+ // Called by BubbleController to notify a bubble of an event that the bubble
+ // might want to close on. Return true if the bubble should close for the
+ // specified reason.
+ virtual bool ShouldClose(BubbleCloseReason reason);
+
+ // Called by BubbleController to build the UI that will represent this bubble.
+ // BubbleDelegate should not keep a reference to this newly created UI.
+ virtual scoped_ptr<BubbleUI> BuildBubbleUI() = 0;
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(BubbleDelegate);
+};
+
+#endif // COMPONENTS_BUBBLE_BUBBLE_DELEGATE_H_
diff --git a/components/bubble/bubble_manager.cc b/components/bubble/bubble_manager.cc
new file mode 100644
index 0000000..8bbed46
--- /dev/null
+++ b/components/bubble/bubble_manager.cc
@@ -0,0 +1,106 @@
+// Copyright 2015 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 "components/bubble/bubble_manager.h"
+
+#include <vector>
+
+#include "components/bubble/bubble_controller.h"
+#include "components/bubble/bubble_delegate.h"
+
+BubbleManager::BubbleManager() : manager_state_(SHOW_BUBBLES) {}
+
+BubbleManager::~BubbleManager() {
+ manager_state_ = NO_MORE_BUBBLES;
+ CloseAllBubbles(BUBBLE_CLOSE_FORCED);
+}
+
+BubbleReference BubbleManager::ShowBubble(scoped_ptr<BubbleDelegate> bubble) {
+ DCHECK(thread_checker_.CalledOnValidThread());
+ DCHECK(bubble);
+ scoped_ptr<BubbleController> controller(
+ new BubbleController(this, bubble.Pass()));
+
+ BubbleReference bubble_ref = controller->AsWeakPtr();
+
+ switch (manager_state_) {
+ case SHOW_BUBBLES:
+ controller->Show();
+ controllers_.push_back(controller.Pass());
+ break;
+ case QUEUE_BUBBLES:
+ show_queue_.push_back(controller.Pass());
+ break;
+ case NO_MORE_BUBBLES:
+ // The controller will be cleaned up and |bubble_ref| will be invalidated.
+ // It's important that the controller is created even though it's
+ // destroyed immediately because it will collect metrics about the bubble.
+ break;
+ }
+
+ return bubble_ref;
+}
+
+bool BubbleManager::CloseBubble(BubbleReference bubble,
+ BubbleCloseReason reason) {
+ DCHECK(thread_checker_.CalledOnValidThread());
+ DCHECK_NE(manager_state_, QUEUE_BUBBLES);
+ if (manager_state_ == SHOW_BUBBLES)
+ manager_state_ = QUEUE_BUBBLES;
+
+ for (auto iter = controllers_.begin(); iter != controllers_.end(); ++iter) {
+ if (*iter == bubble.get()) {
+ bool closed = (*iter)->ShouldClose(reason);
+ if (closed)
+ iter = controllers_.erase(iter);
+ ShowPendingBubbles();
+ return closed;
+ }
+ }
+
+ // Attempting to close a bubble that is already closed or that this manager
+ // doesn't own is a bug.
+ NOTREACHED();
+ return false;
+}
+
+void BubbleManager::CloseAllBubbles(BubbleCloseReason reason) {
+ // The following close reasons don't make sense for multiple bubbles:
+ DCHECK_NE(reason, BUBBLE_CLOSE_ACCEPTED);
+ DCHECK_NE(reason, BUBBLE_CLOSE_CANCELED);
+
+ DCHECK(thread_checker_.CalledOnValidThread());
+ DCHECK_NE(manager_state_, QUEUE_BUBBLES);
+ if (manager_state_ == SHOW_BUBBLES)
+ manager_state_ = QUEUE_BUBBLES;
+
+ for (auto iter = controllers_.begin(); iter != controllers_.end();)
+ iter = (*iter)->ShouldClose(reason) ? controllers_.erase(iter) : iter + 1;
+
+ ShowPendingBubbles();
+}
+
+void BubbleManager::UpdateAllBubbleAnchors() {
+ DCHECK(thread_checker_.CalledOnValidThread());
+ for (auto controller : controllers_)
+ controller->UpdateAnchorPosition();
+}
+
+void BubbleManager::ShowPendingBubbles() {
+ if (manager_state_ == QUEUE_BUBBLES)
+ manager_state_ = SHOW_BUBBLES;
+
+ if (manager_state_ == SHOW_BUBBLES) {
+ for (auto controller : show_queue_)
+ controller->Show();
+
+ controllers_.insert(controllers_.end(), show_queue_.begin(),
+ show_queue_.end());
+
+ show_queue_.weak_clear();
+ } else {
+ // Clear the queue if bubbles can't be shown.
+ show_queue_.clear();
+ }
+}
diff --git a/components/bubble/bubble_manager.h b/components/bubble/bubble_manager.h
new file mode 100644
index 0000000..215441a
--- /dev/null
+++ b/components/bubble/bubble_manager.h
@@ -0,0 +1,69 @@
+// Copyright 2015 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.
+
+#ifndef COMPONENTS_BUBBLE_BUBBLE_MANAGER_H_
+#define COMPONENTS_BUBBLE_BUBBLE_MANAGER_H_
+
+#include "base/memory/scoped_ptr.h"
+#include "base/memory/scoped_vector.h"
+#include "base/memory/weak_ptr.h"
+#include "base/threading/thread_checker.h"
+#include "components/bubble/bubble_close_reason.h"
+
+class BubbleController;
+class BubbleDelegate;
+
+typedef base::WeakPtr<BubbleController> BubbleReference;
+
+// Inherit from BubbleManager to show, update, and close bubbles.
+// Any class that inherits from BubbleManager should capture any events that
+// should dismiss a bubble or update its anchor point.
+// This class assumes that we won't be showing a lot of bubbles simultaneously.
+// TODO(hcarmona): Handle simultaneous bubbles. http://crbug.com/366937
+class BubbleManager {
+ public:
+ // Should be instantiated on the UI thread.
+ BubbleManager();
+ virtual ~BubbleManager();
+
+ // Shows a specific bubble and returns a reference to it.
+ // This reference should be used through the BubbleManager.
+ BubbleReference ShowBubble(scoped_ptr<BubbleDelegate> bubble);
+
+ // Notify a bubble of an event that might trigger close.
+ // Returns true if the bubble was actually closed.
+ bool CloseBubble(BubbleReference bubble, BubbleCloseReason reason);
+
+ // Notify all bubbles of an event that might trigger close.
+ void CloseAllBubbles(BubbleCloseReason reason);
+
+ // Notify all bubbles that their anchor or parent may have changed.
+ void UpdateAllBubbleAnchors();
+
+ private:
+ enum ManagerStates {
+ SHOW_BUBBLES,
+ QUEUE_BUBBLES,
+ NO_MORE_BUBBLES,
+ };
+
+ // Show any bubbles that were added to |show_queue_|.
+ void ShowPendingBubbles();
+
+ // Verify that functions that affect the UI are done on the same thread.
+ base::ThreadChecker thread_checker_;
+
+ // Determines what happens to a bubble when |ShowBubble| is called.
+ ManagerStates manager_state_;
+
+ // The bubbles that are being managed.
+ ScopedVector<BubbleController> controllers_;
+
+ // The bubbles queued to be shown when possible.
+ ScopedVector<BubbleController> show_queue_;
+
+ DISALLOW_COPY_AND_ASSIGN(BubbleManager);
+};
+
+#endif // COMPONENTS_BUBBLE_BUBBLE_MANAGER_H_
diff --git a/components/bubble/bubble_manager_unittest.cc b/components/bubble/bubble_manager_unittest.cc
new file mode 100644
index 0000000..f18c678
--- /dev/null
+++ b/components/bubble/bubble_manager_unittest.cc
@@ -0,0 +1,347 @@
+// Copyright 2015 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 "components/bubble/bubble_manager.h"
+
+#include "components/bubble/bubble_controller.h"
+#include "components/bubble/bubble_delegate.h"
+#include "components/bubble/bubble_ui.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace {
+
+class MockBubbleUI : public BubbleUI {
+ public:
+ MockBubbleUI() {}
+ ~MockBubbleUI() override { Destroyed(); }
+
+ MOCK_METHOD0(Show, void());
+ MOCK_METHOD0(Close, void());
+ MOCK_METHOD0(UpdateAnchorPosition, void());
+
+ // To verify destructor call.
+ MOCK_METHOD0(Destroyed, void());
+};
+
+class MockBubbleDelegate : public BubbleDelegate {
+ public:
+ MockBubbleDelegate() {}
+ ~MockBubbleDelegate() override { Destroyed(); }
+
+ // Default bubble shows UI and closes when asked to close.
+ static scoped_ptr<MockBubbleDelegate> Default();
+
+ // Stubborn bubble shows UI and doesn't want to close.
+ static scoped_ptr<MockBubbleDelegate> Stubborn();
+
+ MOCK_METHOD1(ShouldClose, bool(BubbleCloseReason reason));
+
+ // A scoped_ptr can't be returned in MOCK_METHOD.
+ MOCK_METHOD0(BuildBubbleUIMock, BubbleUI*());
+ scoped_ptr<BubbleUI> BuildBubbleUI() override {
+ return make_scoped_ptr(BuildBubbleUIMock());
+ }
+
+ // To verify destructor call.
+ MOCK_METHOD0(Destroyed, void());
+};
+
+// static
+scoped_ptr<MockBubbleDelegate> MockBubbleDelegate::Default() {
+ MockBubbleDelegate* delegate = new MockBubbleDelegate;
+ EXPECT_CALL(*delegate, BuildBubbleUIMock())
+ .WillOnce(testing::Return(new MockBubbleUI));
+ EXPECT_CALL(*delegate, ShouldClose(testing::_))
+ .WillOnce(testing::Return(true));
+ return make_scoped_ptr(delegate);
+}
+
+// static
+scoped_ptr<MockBubbleDelegate> MockBubbleDelegate::Stubborn() {
+ MockBubbleDelegate* delegate = new MockBubbleDelegate;
+ EXPECT_CALL(*delegate, BuildBubbleUIMock())
+ .WillOnce(testing::Return(new MockBubbleUI));
+ EXPECT_CALL(*delegate, ShouldClose(testing::_))
+ .WillRepeatedly(testing::Return(false));
+ return make_scoped_ptr(delegate);
+}
+
+// Helper class used to test chaining another bubble.
+class DelegateChainHelper {
+ public:
+ DelegateChainHelper(BubbleManager* manager,
+ scoped_ptr<BubbleDelegate> next_delegate);
+
+ // Will show the bubble in |next_delegate_|.
+ void Chain() { manager_->ShowBubble(next_delegate_.Pass()); }
+
+ // True if the bubble was taken by the bubble manager.
+ bool BubbleWasTaken() { return !next_delegate_; }
+
+ private:
+ BubbleManager* manager_; // Weak.
+ scoped_ptr<BubbleDelegate> next_delegate_;
+};
+
+DelegateChainHelper::DelegateChainHelper(
+ BubbleManager* manager,
+ scoped_ptr<BubbleDelegate> next_delegate)
+ : manager_(manager), next_delegate_(next_delegate.Pass()) {}
+
+class BubbleManagerTest : public testing::Test {
+ public:
+ BubbleManagerTest();
+ ~BubbleManagerTest() override {}
+
+ void SetUp() override;
+ void TearDown() override;
+
+ protected:
+ scoped_ptr<BubbleManager> manager_;
+};
+
+BubbleManagerTest::BubbleManagerTest() {}
+
+void BubbleManagerTest::SetUp() {
+ testing::Test::SetUp();
+ manager_.reset(new BubbleManager);
+}
+
+void BubbleManagerTest::TearDown() {
+ manager_.reset();
+ testing::Test::TearDown();
+}
+
+TEST_F(BubbleManagerTest, ManagerShowsBubbleUI) {
+ // Manager will delete bubble_ui.
+ MockBubbleUI* bubble_ui = new MockBubbleUI;
+ EXPECT_CALL(*bubble_ui, Destroyed());
+ EXPECT_CALL(*bubble_ui, Show());
+ EXPECT_CALL(*bubble_ui, Close());
+ EXPECT_CALL(*bubble_ui, UpdateAnchorPosition()).Times(0);
+
+ // Manager will delete delegate.
+ MockBubbleDelegate* delegate = new MockBubbleDelegate;
+ EXPECT_CALL(*delegate, Destroyed());
+ EXPECT_CALL(*delegate, BuildBubbleUIMock())
+ .WillOnce(testing::Return(bubble_ui));
+ EXPECT_CALL(*delegate, ShouldClose(testing::_))
+ .WillOnce(testing::Return(true));
+
+ manager_->ShowBubble(make_scoped_ptr(delegate));
+}
+
+TEST_F(BubbleManagerTest, ManagerUpdatesBubbleUI) {
+ // Manager will delete bubble_ui.
+ MockBubbleUI* bubble_ui = new MockBubbleUI;
+ EXPECT_CALL(*bubble_ui, Destroyed());
+ EXPECT_CALL(*bubble_ui, Show());
+ EXPECT_CALL(*bubble_ui, Close());
+ EXPECT_CALL(*bubble_ui, UpdateAnchorPosition());
+
+ // Manager will delete delegate.
+ MockBubbleDelegate* delegate = new MockBubbleDelegate;
+ EXPECT_CALL(*delegate, Destroyed());
+ EXPECT_CALL(*delegate, BuildBubbleUIMock())
+ .WillOnce(testing::Return(bubble_ui));
+ EXPECT_CALL(*delegate, ShouldClose(testing::_))
+ .WillOnce(testing::Return(true));
+
+ manager_->ShowBubble(make_scoped_ptr(delegate));
+ manager_->UpdateAllBubbleAnchors();
+}
+
+TEST_F(BubbleManagerTest, CloseOnReferenceInvalidatesReference) {
+ BubbleReference ref = manager_->ShowBubble(MockBubbleDelegate::Default());
+
+ ASSERT_TRUE(ref->CloseBubble(BUBBLE_CLOSE_FOCUS_LOST));
+
+ ASSERT_FALSE(ref);
+}
+
+TEST_F(BubbleManagerTest, CloseOnStubbornReferenceDoesNotInvalidate) {
+ BubbleReference ref = manager_->ShowBubble(MockBubbleDelegate::Stubborn());
+
+ ASSERT_FALSE(ref->CloseBubble(BUBBLE_CLOSE_FOCUS_LOST));
+
+ ASSERT_TRUE(ref);
+}
+
+TEST_F(BubbleManagerTest, CloseInvalidatesReference) {
+ BubbleReference ref = manager_->ShowBubble(MockBubbleDelegate::Default());
+
+ ASSERT_TRUE(manager_->CloseBubble(ref, BUBBLE_CLOSE_FOCUS_LOST));
+
+ ASSERT_FALSE(ref);
+}
+
+TEST_F(BubbleManagerTest, CloseAllInvalidatesReference) {
+ BubbleReference ref = manager_->ShowBubble(MockBubbleDelegate::Default());
+
+ manager_->CloseAllBubbles(BUBBLE_CLOSE_FOCUS_LOST);
+
+ ASSERT_FALSE(ref);
+}
+
+TEST_F(BubbleManagerTest, DestroyInvalidatesReference) {
+ BubbleReference ref = manager_->ShowBubble(MockBubbleDelegate::Default());
+
+ manager_.reset();
+
+ ASSERT_FALSE(ref);
+}
+
+TEST_F(BubbleManagerTest, CloseInvalidatesStubbornReference) {
+ BubbleReference ref = manager_->ShowBubble(MockBubbleDelegate::Stubborn());
+
+ ASSERT_TRUE(manager_->CloseBubble(ref, BUBBLE_CLOSE_FORCED));
+
+ ASSERT_FALSE(ref);
+}
+
+TEST_F(BubbleManagerTest, CloseAllInvalidatesStubbornReference) {
+ BubbleReference ref = manager_->ShowBubble(MockBubbleDelegate::Stubborn());
+
+ manager_->CloseAllBubbles(BUBBLE_CLOSE_FORCED);
+
+ ASSERT_FALSE(ref);
+}
+
+TEST_F(BubbleManagerTest, DestroyInvalidatesStubbornReference) {
+ BubbleReference ref = manager_->ShowBubble(MockBubbleDelegate::Stubborn());
+
+ manager_.reset();
+
+ ASSERT_FALSE(ref);
+}
+
+TEST_F(BubbleManagerTest, CloseDoesNotInvalidateStubbornReference) {
+ BubbleReference ref = manager_->ShowBubble(MockBubbleDelegate::Stubborn());
+
+ ASSERT_FALSE(manager_->CloseBubble(ref, BUBBLE_CLOSE_FOCUS_LOST));
+
+ ASSERT_TRUE(ref);
+}
+
+TEST_F(BubbleManagerTest, CloseAllDoesNotInvalidateStubbornReference) {
+ BubbleReference ref = manager_->ShowBubble(MockBubbleDelegate::Stubborn());
+
+ manager_->CloseAllBubbles(BUBBLE_CLOSE_FOCUS_LOST);
+
+ ASSERT_TRUE(ref);
+}
+
+TEST_F(BubbleManagerTest, CloseAllInvalidatesMixAppropriately) {
+ BubbleReference stubborn_ref1 =
+ manager_->ShowBubble(MockBubbleDelegate::Stubborn());
+ BubbleReference normal_ref1 =
+ manager_->ShowBubble(MockBubbleDelegate::Default());
+ BubbleReference stubborn_ref2 =
+ manager_->ShowBubble(MockBubbleDelegate::Stubborn());
+ BubbleReference normal_ref2 =
+ manager_->ShowBubble(MockBubbleDelegate::Default());
+ BubbleReference stubborn_ref3 =
+ manager_->ShowBubble(MockBubbleDelegate::Stubborn());
+ BubbleReference normal_ref3 =
+ manager_->ShowBubble(MockBubbleDelegate::Default());
+
+ manager_->CloseAllBubbles(BUBBLE_CLOSE_FOCUS_LOST);
+
+ ASSERT_TRUE(stubborn_ref1);
+ ASSERT_TRUE(stubborn_ref2);
+ ASSERT_TRUE(stubborn_ref3);
+ ASSERT_FALSE(normal_ref1);
+ ASSERT_FALSE(normal_ref2);
+ ASSERT_FALSE(normal_ref3);
+}
+
+TEST_F(BubbleManagerTest, UpdateAllShouldWorkWithoutBubbles) {
+ // Manager shouldn't crash if bubbles have never been added.
+ manager_->UpdateAllBubbleAnchors();
+
+ // Add a bubble and close it.
+ BubbleReference ref = manager_->ShowBubble(MockBubbleDelegate::Default());
+ ASSERT_TRUE(manager_->CloseBubble(ref, BUBBLE_CLOSE_FORCED));
+
+ // Bubble should NOT get an update event because it's already closed.
+ manager_->UpdateAllBubbleAnchors();
+}
+
+TEST_F(BubbleManagerTest, CloseAllShouldWorkWithoutBubbles) {
+ // Manager shouldn't crash if bubbles have never been added.
+ manager_->CloseAllBubbles(BUBBLE_CLOSE_FOCUS_LOST);
+
+ // Add a bubble and close it.
+ BubbleReference ref = manager_->ShowBubble(MockBubbleDelegate::Default());
+ ASSERT_TRUE(manager_->CloseBubble(ref, BUBBLE_CLOSE_FORCED));
+
+ // Bubble should NOT get a close event because it's already closed.
+ manager_->CloseAllBubbles(BUBBLE_CLOSE_FOCUS_LOST);
+}
+
+TEST_F(BubbleManagerTest, AllowBubbleChainingOnClose) {
+ scoped_ptr<BubbleDelegate> chained_delegate = MockBubbleDelegate::Default();
+ DelegateChainHelper chain_helper(manager_.get(), chained_delegate.Pass());
+
+ // Manager will delete delegate.
+ MockBubbleDelegate* delegate = new MockBubbleDelegate;
+ EXPECT_CALL(*delegate, BuildBubbleUIMock())
+ .WillOnce(testing::Return(new MockBubbleUI));
+ EXPECT_CALL(*delegate, ShouldClose(testing::_))
+ .WillOnce(testing::DoAll(testing::InvokeWithoutArgs(
+ &chain_helper, &DelegateChainHelper::Chain),
+ testing::Return(true)));
+
+ BubbleReference ref = manager_->ShowBubble(make_scoped_ptr(delegate));
+ ASSERT_TRUE(manager_->CloseBubble(ref, BUBBLE_CLOSE_FORCED));
+
+ ASSERT_TRUE(chain_helper.BubbleWasTaken());
+}
+
+TEST_F(BubbleManagerTest, AllowBubbleChainingOnCloseAll) {
+ scoped_ptr<BubbleDelegate> chained_delegate = MockBubbleDelegate::Default();
+ DelegateChainHelper chain_helper(manager_.get(), chained_delegate.Pass());
+
+ // Manager will delete delegate.
+ MockBubbleDelegate* delegate = new MockBubbleDelegate;
+ EXPECT_CALL(*delegate, BuildBubbleUIMock())
+ .WillOnce(testing::Return(new MockBubbleUI));
+ EXPECT_CALL(*delegate, ShouldClose(testing::_))
+ .WillOnce(testing::DoAll(testing::InvokeWithoutArgs(
+ &chain_helper, &DelegateChainHelper::Chain),
+ testing::Return(true)));
+
+ manager_->ShowBubble(make_scoped_ptr(delegate));
+ manager_->CloseAllBubbles(BUBBLE_CLOSE_FORCED);
+
+ ASSERT_TRUE(chain_helper.BubbleWasTaken());
+}
+
+TEST_F(BubbleManagerTest, BubblesDoNotChainOnDestroy) {
+ // Manager will delete delegate.
+ MockBubbleDelegate* chained_delegate = new MockBubbleDelegate;
+ EXPECT_CALL(*chained_delegate, BuildBubbleUIMock()).Times(0);
+ EXPECT_CALL(*chained_delegate, ShouldClose(testing::_)).Times(0);
+
+ DelegateChainHelper chain_helper(manager_.get(),
+ make_scoped_ptr(chained_delegate));
+
+ // Manager will delete delegate.
+ MockBubbleDelegate* delegate = new MockBubbleDelegate;
+ EXPECT_CALL(*delegate, BuildBubbleUIMock())
+ .WillOnce(testing::Return(new MockBubbleUI));
+ EXPECT_CALL(*delegate, ShouldClose(testing::_))
+ .WillOnce(testing::DoAll(testing::InvokeWithoutArgs(
+ &chain_helper, &DelegateChainHelper::Chain),
+ testing::Return(true)));
+
+ manager_->ShowBubble(make_scoped_ptr(delegate));
+ manager_.reset();
+
+ // The manager will take the bubble, but not show it.
+ ASSERT_TRUE(chain_helper.BubbleWasTaken());
+}
+
+} // namespace
diff --git a/components/bubble/bubble_ui.h b/components/bubble/bubble_ui.h
new file mode 100644
index 0000000..5c52f4fb
--- /dev/null
+++ b/components/bubble/bubble_ui.h
@@ -0,0 +1,24 @@
+// Copyright 2015 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.
+
+#ifndef COMPONENTS_BUBBLE_BUBBLE_UI_H_
+#define COMPONENTS_BUBBLE_BUBBLE_UI_H_
+
+class BubbleUI {
+ public:
+ virtual ~BubbleUI() {}
+
+ // Should display the bubble UI.
+ virtual void Show() = 0;
+
+ // Should close the bubble UI.
+ virtual void Close() = 0;
+
+ // Should update the bubble UI's position.
+ // Important to verify that an anchor is still available.
+ // ex: fullscreen might not have a location bar in views.
+ virtual void UpdateAnchorPosition() = 0;
+};
+
+#endif // COMPONENTS_BUBBLE_BUBBLE_UI_H_
diff --git a/components/components.gyp b/components/components.gyp
index ace5a1a..b0be14d 100644
--- a/components/components.gyp
+++ b/components/components.gyp
@@ -14,6 +14,7 @@
'auto_login_parser.gypi',
'autofill.gypi',
'bookmarks.gypi',
+ 'bubble.gypi',
'captive_portal.gypi',
'cloud_devices.gypi',
'component_updater.gypi',
diff --git a/components/components_tests.gyp b/components/components_tests.gyp
index 09029ca..b647e26 100644
--- a/components/components_tests.gyp
+++ b/components/components_tests.gyp
@@ -81,6 +81,9 @@
'browser_watcher/watcher_metrics_provider_win_unittest.cc',
'browser_watcher/window_hang_monitor_win_unittest.cc',
],
+ 'bubble_unittest_sources': [
+ 'bubble/bubble_manager_unittest.cc',
+ ],
'captive_portal_unittest_sources': [
'captive_portal/captive_portal_detector_unittest.cc',
],
@@ -754,6 +757,7 @@
'<@(autofill_unittest_sources)',
'<@(bookmarks_unittest_sources)',
'<@(browser_watcher_unittest_sources)',
+ '<@(bubble_unittest_sources)',
'<@(captive_portal_unittest_sources)',
'<@(cloud_devices_unittest_sources)',
'<@(component_updater_unittest_sources)',
@@ -849,6 +853,7 @@
'components.gyp:bookmarks_browser',
'components.gyp:bookmarks_managed',
'components.gyp:bookmarks_test_support',
+ 'components.gyp:bubble',
'components.gyp:captive_portal_test_support',
'components.gyp:cloud_devices_common',
'components.gyp:component_updater',