diff options
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', |