diff options
25 files changed, 405 insertions, 97 deletions
diff --git a/chrome/browser/browser.cc b/chrome/browser/browser.cc index 92bf8eb..3259fd6 100644 --- a/chrome/browser/browser.cc +++ b/chrome/browser/browser.cc @@ -178,8 +178,8 @@ Browser::Browser(Type type, Profile* profile) } Browser::~Browser() { - // The tab strip should be empty at this point. - DCHECK(tabstrip_model_.empty()); + // The tab strip should not have any significant tabs at this point. + DCHECK(!tabstrip_model_.HasNonPhantomTabs()); tabstrip_model_.RemoveObserver(this); BrowserList::RemoveBrowser(this); @@ -1854,7 +1854,7 @@ void Browser::TabInsertedAt(TabContents* contents, contents->set_delegate(this); contents->controller().SetWindowID(session_id()); - SyncHistoryWithTabs(tabstrip_model_.GetIndexOfTabContents(contents)); + SyncHistoryWithTabs(index); // Make sure the loading state is updated correctly, otherwise the throbber // won't start if the page is loading. @@ -1877,20 +1877,7 @@ void Browser::TabClosingAt(TabContents* contents, int index) { } void Browser::TabDetachedAt(TabContents* contents, int index) { - // Save what the user's currently typing. - window_->GetLocationBar()->SaveStateToContents(contents); - - contents->set_delegate(NULL); - if (!tabstrip_model_.closing_all()) - SyncHistoryWithTabs(0); - - RemoveScheduledUpdatesFor(contents); - - if (find_bar_controller_.get() && index == tabstrip_model_.selected_index()) - find_bar_controller_->ChangeTabContents(NULL); - - registrar_.Remove(this, NotificationType::TAB_CONTENTS_DISCONNECTED, - Source<TabContents>(contents)); + TabDetachedAtImpl(contents, index, DETACH_TYPE_DETACH); } void Browser::TabDeselectedAt(TabContents* contents, int index) { @@ -1952,6 +1939,21 @@ void Browser::TabMoved(TabContents* contents, SyncHistoryWithTabs(std::min(from_index, to_index)); } +void Browser::TabReplacedAt(TabContents* old_contents, + TabContents* new_contents, int index) { + TabDetachedAtImpl(old_contents, index, DETACH_TYPE_REPLACE); + TabInsertedAt(new_contents, index, + (index == tabstrip_model_.selected_index())); + + int entry_count = new_contents->controller().entry_count(); + if (entry_count > 0) { + // Send out notification so that observers are updated appropriately. + new_contents->controller().NotifyEntryChanged( + new_contents->controller().GetEntryAtIndex(entry_count - 1), + entry_count - 1); + } +} + void Browser::TabPinnedStateChanged(TabContents* contents, int index) { if (!profile()->HasSessionService()) return; @@ -3178,6 +3180,26 @@ void Browser::CloseFrame() { window_->Close(); } +void Browser::TabDetachedAtImpl(TabContents* contents, int index, + DetachType type) { + if (type == DETACH_TYPE_DETACH) { + // Save what the user's currently typed. + window_->GetLocationBar()->SaveStateToContents(contents); + + if (!tabstrip_model_.closing_all()) + SyncHistoryWithTabs(0); + } + + contents->set_delegate(NULL); + RemoveScheduledUpdatesFor(contents); + + if (find_bar_controller_.get() && index == tabstrip_model_.selected_index()) + find_bar_controller_->ChangeTabContents(NULL); + + registrar_.Remove(this, NotificationType::TAB_CONTENTS_DISCONNECTED, + Source<TabContents>(contents)); +} + // static void Browser::RegisterAppPrefs(const std::wstring& app_name) { // A set of apps that we've already started. diff --git a/chrome/browser/browser.h b/chrome/browser/browser.h index 50fd49d..e4effcb 100644 --- a/chrome/browser/browser.h +++ b/chrome/browser/browser.h @@ -500,6 +500,21 @@ class Browser : public TabStripModelDelegate, virtual void ExecuteCommand(int id); private: + FRIEND_TEST(BrowserTest, NoTabsInPopups); + + // Used to describe why a tab is being detached. This is used by + // TabDetachedAtImpl. + enum DetachType { + // Result of TabDetachedAt. + DETACH_TYPE_DETACH, + + // Result of TabReplacedAt. + DETACH_TYPE_REPLACE, + + // Result of the tab strip not having any significant tabs. + DETACH_TYPE_EMPTY + }; + // Overridden from TabStripModelDelegate: virtual TabContents* AddBlankTab(bool foreground); virtual TabContents* AddBlankTabAt(int index, bool foreground); @@ -542,6 +557,9 @@ class Browser : public TabStripModelDelegate, int from_index, int to_index, bool pinned_state_changed); + virtual void TabReplacedAt(TabContents* old_contents, + TabContents* new_contents, + int index); virtual void TabPinnedStateChanged(TabContents* contents, int index); virtual void TabStripEmpty(); @@ -730,7 +748,7 @@ class Browser : public TabStripModelDelegate, // after a return to the message loop. void CloseFrame(); - FRIEND_TEST(BrowserTest, NoTabsInPopups); + void TabDetachedAtImpl(TabContents* contents, int index, DetachType type); // Create a preference dictionary for the provided application name. This is // done only once per application name / per session. diff --git a/chrome/browser/browser_browsertest.cc b/chrome/browser/browser_browsertest.cc index 35a04da5..7146210 100644 --- a/chrome/browser/browser_browsertest.cc +++ b/chrome/browser/browser_browsertest.cc @@ -10,6 +10,7 @@ #include "chrome/browser/app_modal_dialog.h" #include "chrome/browser/browser.h" #include "chrome/browser/browser_process.h" +#include "chrome/browser/defaults.h" #include "chrome/browser/renderer_host/render_process_host.h" #include "chrome/browser/tab_contents/tab_contents.h" #include "chrome/common/url_constants.h" @@ -58,6 +59,35 @@ int CountRenderProcessHosts() { } // namespace class BrowserTest : public InProcessBrowserTest { + public: + // Used by phantom tab tests. Creates two tabs, pins the first and makes it + // a phantom tab (by closing it). + void PhantomTabTest() { + static const wchar_t kDocRoot[] = L"chrome/test/data"; + scoped_refptr<HTTPTestServer> server( + HTTPTestServer::CreateServer(kDocRoot, NULL)); + ASSERT_TRUE(NULL != server.get()); + GURL url(server->TestServerPage("empty.html")); + TabStripModel* model = browser()->tabstrip_model(); + + ui_test_utils::NavigateToURL(browser(), url); + model->SetTabPinned(0, true); + + browser()->AddTabWithURL(GURL("about:blank"), GURL(), PageTransition::TYPED, + true, 1, false, NULL); + ui_test_utils::NavigateToURL(browser(), url); + + // Close the first, which should make it a phantom. + TabContents* initial_contents = model->GetTabContentsAt(0); + model->CloseTabContentsAt(0); + // There should still be two tabs. + ASSERT_EQ(2, browser()->tab_count()); + // The first tab should be a phantom. + EXPECT_TRUE(model->IsPhantomTab(0)); + // And the tab contents of the first tab should have changed. + EXPECT_TRUE(model->GetTabContentsAt(0) != initial_contents); + } + protected: // In RTL locales wrap the page title with RTL embedding characters so that it // matches the value returned by GetWindowTitle(). @@ -293,6 +323,36 @@ IN_PROC_BROWSER_TEST_F(BrowserTest, FaviconOfOnloadRedirectToAnchorPage) { EXPECT_EQ(expected_favicon_url.spec(), entry->favicon().url().spec()); } +// TODO(sky): enable these once phantom tabs aren't behind a flag. +/* +IN_PROC_BROWSER_TEST_F(BrowserTest, PhantomTab) { + if (!browser_defaults::kPinnedTabsActLikeApps) + return; + + PhantomTabTest(); +} + +IN_PROC_BROWSER_TEST_F(BrowserTest, RevivePhantomTab) { + if (!browser_defaults::kPinnedTabsActLikeApps) + return; + + PhantomTabTest(); + + if (HasFatalFailure()) + return; + + TabStripModel* model = browser()->tabstrip_model(); + + // Revive the phantom tab by selecting it. + browser()->SelectTabContentsAt(0, true); + + // There should still be two tabs. + ASSERT_EQ(2, browser()->tab_count()); + // The first tab should no longer be a phantom. + EXPECT_FALSE(model->IsPhantomTab(0)); +} +*/ + // Tests that the CLD (Compact Language Detection) works properly. IN_PROC_BROWSER_TEST_F(BrowserTest, PageLanguageDetection) { static const wchar_t kDocRoot[] = L"chrome/test/data"; diff --git a/chrome/browser/cocoa/browser_window_controller.mm b/chrome/browser/cocoa/browser_window_controller.mm index de344ad..64c9f26 100644 --- a/chrome/browser/cocoa/browser_window_controller.mm +++ b/chrome/browser/cocoa/browser_window_controller.mm @@ -420,7 +420,7 @@ willPositionSheet:(NSWindow*)sheet // have to save the window position before we call orderOut:. [self saveWindowPositionIfNeeded]; - if (!browser_->tabstrip_model()->empty()) { + if (browser_->tabstrip_model()->HasNonPhantomTabs()) { // Tab strip isn't empty. Hide the frame (so it appears to have closed // immediately) and close all the tabs, allowing the renderers to shut // down. When the tab strip is empty we'll be called back again. diff --git a/chrome/browser/defaults.cc b/chrome/browser/defaults.cc index a901949..080ec265 100644 --- a/chrome/browser/defaults.cc +++ b/chrome/browser/defaults.cc @@ -6,6 +6,12 @@ namespace browser_defaults { +#if defined(TOOLKIT_VIEWS) +const bool kPinnedTabsActLikeApps = true; +#else +const bool kPinnedTabsActLikeApps = false; +#endif + #if defined(OS_CHROMEOS) const double kAutocompleteEditFontPixelSize = 12.0; diff --git a/chrome/browser/defaults.h b/chrome/browser/defaults.h index 595bb70..78c7be4 100644 --- a/chrome/browser/defaults.h +++ b/chrome/browser/defaults.h @@ -60,6 +60,9 @@ extern const bool kDownloadPageHasShowInFolder; // Should the tab strip be sized to the top of the tab strip? extern const bool kSizeTabButtonToTopOfTabStrip; +// Should pinned tabs be treated as though they are apps as well? +extern const bool kPinnedTabsActLikeApps; + // Whether we should bootstrap the sync authentication using cookies instead of // asking the user for credentials. extern const bool kBootstrapSyncAuthentication; diff --git a/chrome/browser/extensions/extension_browser_event_router.cc b/chrome/browser/extensions/extension_browser_event_router.cc index af03497..667c9e3 100644 --- a/chrome/browser/extensions/extension_browser_event_router.cc +++ b/chrome/browser/extensions/extension_browser_event_router.cc @@ -396,6 +396,12 @@ void ExtensionBrowserEventRouter::TabChangedAt(TabContents* contents, TabUpdated(contents, false); } +void ExtensionBrowserEventRouter::TabReplacedAt(TabContents* old_contents, + TabContents* new_contents, + int index) { + // TODO: figure out the right notification to send. +} + void ExtensionBrowserEventRouter::TabStripEmpty() { } void ExtensionBrowserEventRouter::DispatchOldPageActionEvent( diff --git a/chrome/browser/extensions/extension_browser_event_router.h b/chrome/browser/extensions/extension_browser_event_router.h index a740f58..0b56c45 100644 --- a/chrome/browser/extensions/extension_browser_event_router.h +++ b/chrome/browser/extensions/extension_browser_event_router.h @@ -40,18 +40,20 @@ class ExtensionBrowserEventRouter : public TabStripModelObserver, void OnBrowserWindowReady(const Browser* browser); // TabStripModelObserver - void TabInsertedAt(TabContents* contents, int index, bool foreground); - void TabClosingAt(TabContents* contents, int index); - void TabDetachedAt(TabContents* contents, int index); - void TabSelectedAt(TabContents* old_contents, - TabContents* new_contents, - int index, - bool user_gesture); - void TabMoved(TabContents* contents, int from_index, int to_index, - bool pinned_state_changed); - void TabChangedAt(TabContents* contents, int index, - TabChangeType change_type); - void TabStripEmpty(); + virtual void TabInsertedAt(TabContents* contents, int index, bool foreground); + virtual void TabClosingAt(TabContents* contents, int index); + virtual void TabDetachedAt(TabContents* contents, int index); + virtual void TabSelectedAt(TabContents* old_contents, + TabContents* new_contents, + int index, + bool user_gesture); + virtual void TabMoved(TabContents* contents, int from_index, int to_index, + bool pinned_state_changed); + virtual void TabChangedAt(TabContents* contents, int index, + TabChangeType change_type); + virtual void TabReplacedAt(TabContents* old_contents, + TabContents* new_contents, int index); + virtual void TabStripEmpty(); // Page Action execute event. void PageActionExecuted(Profile* profile, diff --git a/chrome/browser/gtk/browser_window_gtk.cc b/chrome/browser/gtk/browser_window_gtk.cc index 3a2d070..1e2a448 100644 --- a/chrome/browser/gtk/browser_window_gtk.cc +++ b/chrome/browser/gtk/browser_window_gtk.cc @@ -1261,7 +1261,7 @@ bool BrowserWindowGtk::CanClose() const { if (!browser_->ShouldCloseWindow()) return false; - if (!browser_->tabstrip_model()->empty()) { + if (browser_->tabstrip_model()->HasNonPhantomTabs()) { // Tab strip isn't empty. Hide the window (so it appears to have closed // immediately) and close all the tabs, allowing the renderers to shut // down. When the tab strip is empty we'll be called back again. diff --git a/chrome/browser/gtk/tabs/dragged_tab_controller_gtk.cc b/chrome/browser/gtk/tabs/dragged_tab_controller_gtk.cc index 65fa966..415710c 100644 --- a/chrome/browser/gtk/tabs/dragged_tab_controller_gtk.cc +++ b/chrome/browser/gtk/tabs/dragged_tab_controller_gtk.cc @@ -556,7 +556,7 @@ void DraggedTabControllerGtk::Detach() { } // If we've removed the last tab from the tabstrip, hide the frame now. - if (attached_model->empty()) + if (!attached_model->HasNonPhantomTabs()) HideWindow(); // Update the dragged tab. This NULL check is necessary apparently in some @@ -865,7 +865,7 @@ void DraggedTabControllerGtk::ShowWindow() { void DraggedTabControllerGtk::CleanUpHiddenFrame() { // If the model we started dragging from is now empty, we must ask the // delegate to close the frame. - if (source_tabstrip_->model()->empty()) + if (!source_tabstrip_->model()->HasNonPhantomTabs()) source_tabstrip_->model()->delegate()->CloseFrameAfterDragSession(); } diff --git a/chrome/browser/tab_contents/tab_contents.cc b/chrome/browser/tab_contents/tab_contents.cc index f4bc6d4..7d141b6 100644 --- a/chrome/browser/tab_contents/tab_contents.cc +++ b/chrome/browser/tab_contents/tab_contents.cc @@ -47,6 +47,7 @@ #include "chrome/browser/renderer_host/site_instance.h" #include "chrome/browser/renderer_host/web_cache_manager.h" #include "chrome/browser/renderer_preferences_util.h" +#include "chrome/browser/sessions/session_types.h" #include "chrome/browser/tab_contents/infobar_delegate.h" #include "chrome/browser/tab_contents/interstitial_page.h" #include "chrome/browser/tab_contents/navigation_entry.h" @@ -1193,6 +1194,27 @@ void TabContents::OnCloseStarted() { tab_close_start_time_ = base::TimeTicks::Now(); } +TabContents* TabContents::CloneAndMakePhantom() { + // TODO: the initial URL, title and what not should come from the app. + NavigationEntry* entry = controller().GetActiveEntry(); + + TabNavigation tab_nav; + if (entry) + tab_nav.SetFromNavigationEntry(*entry); + std::vector<TabNavigation> navigations; + navigations.push_back(tab_nav); + + TabContents* new_contents = + new TabContents(profile(), NULL, MSG_ROUTING_NONE, NULL); + new_contents->controller().RestoreFromState(navigations, 0, false); + if (entry) { + // TODO: this should come from the app. + new_contents->controller().GetActiveEntry()->favicon() = entry->favicon(); + } + + return new_contents; +} + // Notifies the RenderWidgetHost instance about the fact that the page is // loading, or done loading and calls the base implementation. void TabContents::SetIsLoading(bool is_loading, diff --git a/chrome/browser/tab_contents/tab_contents.h b/chrome/browser/tab_contents/tab_contents.h index 1418684..0603d6b 100644 --- a/chrome/browser/tab_contents/tab_contents.h +++ b/chrome/browser/tab_contents/tab_contents.h @@ -639,6 +639,13 @@ class TabContents : public PageNavigator, return request_context_; } + // Creates a duplicate of this TabContents. The returned TabContents is + // configured such that the renderer has not been loaded (it'll load the first + // time it is selected). + // This is intended for use with apps. + // The caller owns the returned object. + TabContents* CloneAndMakePhantom(); + // JavaScriptMessageBoxClient ------------------------------------------------ virtual std::wstring GetMessageBoxTitle(const GURL& frame_url, bool is_alert); diff --git a/chrome/browser/tabs/tab_strip_model.cc b/chrome/browser/tabs/tab_strip_model.cc index 56ec106..39f4b66 100644 --- a/chrome/browser/tabs/tab_strip_model.cc +++ b/chrome/browser/tabs/tab_strip_model.cc @@ -12,6 +12,7 @@ #include "build/build_config.h" #include "chrome/browser/bookmarks/bookmark_model.h" #include "chrome/browser/browser_shutdown.h" +#include "chrome/browser/defaults.h" #include "chrome/browser/metrics/user_metrics.h" #include "chrome/browser/profile.h" #include "chrome/browser/renderer_host/render_process_host.h" @@ -71,6 +72,14 @@ void TabStripModel::RemoveObserver(TabStripModelObserver* observer) { observers_.RemoveObserver(observer); } +bool TabStripModel::HasNonPhantomTabs() const { + for (int i = 0; i < count(); i++) { + if (!IsPhantomTab(i)) + return true; + } + return false; +} + bool TabStripModel::HasObserver(TabStripModelObserver* observer) { for (size_t i = 0; i < observers_.size(); ++i) { if (observers_.GetElementAt(i) == observer) @@ -147,18 +156,20 @@ TabContents* TabStripModel::DetachTabContentsAt(int index) { DCHECK(ContainsIndex(index)); TabContents* removed_contents = GetContentsAt(index); - next_selected_index_ = order_controller_->DetermineNewSelectedIndex(index); + next_selected_index_ = + order_controller_->DetermineNewSelectedIndex(index, true); + next_selected_index_ = IndexOfNextNonPhantomTab(next_selected_index_, -1); delete contents_data_.at(index); contents_data_.erase(contents_data_.begin() + index); - if (contents_data_.empty()) + if (!HasNonPhantomTabs()) closing_all_ = true; TabStripModelObservers::Iterator iter(observers_); while (TabStripModelObserver* obs = iter.GetNext()) { obs->TabDetachedAt(removed_contents, index); - if (empty()) + if (!HasNonPhantomTabs()) obs->TabStripEmpty(); } - if (!contents_data_.empty()) { + if (HasNonPhantomTabs()) { if (index == selected_index_) { ChangeSelectedContentsFrom(removed_contents, next_selected_index_, false); @@ -257,20 +268,21 @@ int TabStripModel::GetIndexOfNextTabContentsOpenedBy( DCHECK(opener); DCHECK(ContainsIndex(start_index)); - TabContentsData* start_data = contents_data_.at(start_index); TabContentsDataVector::const_iterator iter = - find(contents_data_.begin(), contents_data_.end(), start_data); + contents_data_.begin() + start_index; TabContentsDataVector::const_iterator next; for (; iter != contents_data_.end(); ++iter) { next = iter + 1; if (next == contents_data_.end()) break; - if (OpenerMatches(*next, opener, use_group)) + if (OpenerMatches(*next, opener, use_group) && + !IsPhantomTab(static_cast<int>(next - contents_data_.begin()))) { return static_cast<int>(next - contents_data_.begin()); + } } - iter = find(contents_data_.begin(), contents_data_.end(), start_data); + iter = contents_data_.begin() + start_index; if (iter != contents_data_.begin()) { - for (--iter; iter > contents_data_.begin(); --iter) { + for (--iter; iter != contents_data_.begin(); --iter) { if (OpenerMatches(*iter, opener, use_group)) return static_cast<int>(iter - contents_data_.begin()); } @@ -283,18 +295,18 @@ int TabStripModel::GetIndexOfLastTabContentsOpenedBy( DCHECK(opener); DCHECK(ContainsIndex(start_index)); - TabContentsData* start_data = contents_data_.at(start_index); TabContentsDataVector::const_iterator end = - find(contents_data_.begin(), contents_data_.end(), start_data); - TabContentsDataVector::const_iterator iter = - contents_data_.end(); + contents_data_.begin() + start_index; + TabContentsDataVector::const_iterator iter = contents_data_.end(); TabContentsDataVector::const_iterator next; for (; iter != end; --iter) { next = iter - 1; if (next == end) break; - if ((*next)->opener == opener) + if ((*next)->opener == opener && + !IsPhantomTab(static_cast<int>(next - contents_data_.begin()))) { return static_cast<int>(next - contents_data_.begin()); + } } return kNoTab; } @@ -383,6 +395,19 @@ bool TabStripModel::IsTabPinned(int index) const { return contents_data_[index]->pinned; } +bool TabStripModel::IsAppTab(int index) const { + // TODO (sky): this is temporary and should be integrated with real apps. + return CommandLine::ForCurrentProcess()->HasSwitch( + switches::kEnablePhantomTabs) && + browser_defaults::kPinnedTabsActLikeApps && + IsTabPinned(index); +} + +bool TabStripModel::IsPhantomTab(int index) const { + return IsTabPinned(index) && IsAppTab(index) && + GetTabContentsAt(index)->controller().needs_reload(); +} + bool TabStripModel::IsTabBlocked(int index) const { return contents_data_[index]->blocked; } @@ -461,20 +486,11 @@ void TabStripModel::CloseSelectedTab() { } void TabStripModel::SelectNextTab() { - // This may happen during automated testing or if a user somehow buffers - // many key accelerators. - if (empty()) - return; - - int next_index = (selected_index_ + 1) % count(); - SelectTabContentsAt(next_index, true); + SelectRelativeTab(true); } void TabStripModel::SelectPreviousTab() { - int prev_index = selected_index_ - 1; - if (prev_index < 0) - prev_index = count() + prev_index; - SelectTabContentsAt(prev_index, true); + SelectRelativeTab(false); } void TabStripModel::SelectLastTab() { @@ -562,7 +578,7 @@ void TabStripModel::ExecuteContextMenuCommand( TabContents* contents = GetTabContentsAt(context_index); std::vector<int> closing_tabs; for (int i = count() - 1; i >= 0; --i) { - if (GetTabContentsAt(i) != contents) + if (GetTabContentsAt(i) != contents && !IsTabPinned(i)) closing_tabs.push_back(i); } InternalCloseTabs(closing_tabs, true); @@ -572,7 +588,8 @@ void TabStripModel::ExecuteContextMenuCommand( UserMetrics::RecordAction("TabContextMenu_CloseTabsToRight", profile_); std::vector<int> closing_tabs; for (int i = count() - 1; i > context_index; --i) { - closing_tabs.push_back(i); + if (!IsTabPinned(i)) + closing_tabs.push_back(i); } InternalCloseTabs(closing_tabs, true); break; @@ -580,6 +597,13 @@ void TabStripModel::ExecuteContextMenuCommand( case CommandCloseTabsOpenedBy: { UserMetrics::RecordAction("TabContextMenu_CloseTabsOpenedBy", profile_); std::vector<int> closing_tabs = GetIndexesOpenedBy(context_index); + for (std::vector<int>::iterator i = closing_tabs.begin(); + i != closing_tabs.end();) { + if (IsTabPinned(*i)) + i = closing_tabs.erase(i); + else + ++i; + } InternalCloseTabs(closing_tabs, true); break; } @@ -631,7 +655,14 @@ void TabStripModel::Observe(NotificationType type, if (index != TabStripModel::kNoTab) { // Note that we only detach the contents here, not close it - it's already // been closed. We just want to undo our bookkeeping. - DetachTabContentsAt(index); + if (IsAppTab(index) && IsTabPinned(index) && !IsPhantomTab(index) && + !closing_all_) { + // We don't actually allow pinned tabs to close. Instead they become + // phantom. + MakePhantom(index); + } else { + DetachTabContentsAt(index); + } } } @@ -776,6 +807,65 @@ void TabStripModel::SetOpenerForContents(TabContents* contents, contents_data_.at(index)->opener = &opener->controller(); } +void TabStripModel::SelectRelativeTab(bool next) { + // This may happen during automated testing or if a user somehow buffers + // many key accelerators. + if (contents_data_.empty()) + return; + + // Skip pinned-app-phantom tabs when iterating. + int index = selected_index_; + int delta = next ? 1 : -1; + do { + index = (index + count() + delta) % count(); + } while (index != selected_index_ && IsTabPinned(index) && IsAppTab(index) && + IsPhantomTab(index)); + SelectTabContentsAt(index, true); +} + +int TabStripModel::IndexOfNextNonPhantomTab(int index, + int ignore_index) { + if (index == kNoTab) + return kNoTab; + + if (empty()) + return index; + + index = std::min(count() - 1, std::max(0, index)); + int start = index; + do { + if (index != ignore_index && !IsPhantomTab(index)) + return index; + index = (index + 1) % count(); + } while (index != start); + + // All phantom tabs. + return start; +} + +void TabStripModel::MakePhantom(int index) { + if (selected_index_ == index) { + // Change the selection, otherwise we're going to force the phantom tab + // to become selected. + // NOTE: we must do this before switching the TabContents, otherwise + // observers are notified with the wrong tab contents. + int new_selected_index = + order_controller_->DetermineNewSelectedIndex(index, false); + new_selected_index = IndexOfNextNonPhantomTab(new_selected_index, + index); + SelectTabContentsAt(new_selected_index, true); + } + + TabContents* old_contents = GetContentsAt(index); + TabContents* new_contents = old_contents->CloneAndMakePhantom(); + + contents_data_[index]->contents = new_contents; + + // And notify observers. + FOR_EACH_OBSERVER(TabStripModelObserver, observers_, + TabReplacedAt(old_contents, new_contents, index)); +} + // static bool TabStripModel::OpenerMatches(const TabContentsData* data, const NavigationController* opener, diff --git a/chrome/browser/tabs/tab_strip_model.h b/chrome/browser/tabs/tab_strip_model.h index bdd92c3..788aaa3 100644 --- a/chrome/browser/tabs/tab_strip_model.h +++ b/chrome/browser/tabs/tab_strip_model.h @@ -99,6 +99,12 @@ class TabStripModelObserver { virtual void TabChangedAt(TabContents* contents, int index, TabChangeType change_type) {} + // The tab contents was replaced at the specified index. This is invoked when + // a tab becomes phantom. See description of phantom tabs in class description + // of TabStripModel for details. + virtual void TabReplacedAt(TabContents* old_contents, + TabContents* new_contents, int index) {} + // Invoked when the pinned state of a tab changes. // NOTE: this is only invoked if the tab doesn't move as a result of its // pinned state changing. If the tab moves as a result, the observer is @@ -110,10 +116,10 @@ class TabStripModelObserver { // window. virtual void TabBlockedStateChanged(TabContents* contents, int index) { } - // The TabStripModel now no longer has any "significant" (user created or - // user manipulated) tabs. The implementer may use this as a trigger to try - // and close the window containing the TabStripModel, for example... - virtual void TabStripEmpty() { } + // The TabStripModel now no longer has any phantom tabs. The implementer may + // use this as a trigger to try and close the window containing the + // TabStripModel, for example... + virtual void TabStripEmpty() {} }; /////////////////////////////////////////////////////////////////////////////// @@ -227,15 +233,26 @@ class TabStripModelDelegate { // them, as well as a higher level API for doing specific Browser-related // tasks like adding new Tabs from just a URL, etc. // -// Each tab may additionally be pinned. The view typically renders pinned tabs -// differently. The model makes sure all pinned tabs are organized at the -// beginning of the tabstrip. Inserting a tab between pinned tabs -// implicitly makes the inserted tab pinned. Similarly moving a tab may pin or -// unpin the tab, again enforcing that all pinned tabs occur at the beginning -// of the tabstrip. Lastly, changing the pinned state of a tab moves the -// tab to be grouped with the pinned or unpinned tabs. For example, if the -// first two tabs are pinned, and the tenth tab is pinned, it is moved to -// become the third tab. +// Each tab may be any one of the following states: +// . Pinned. The view typically renders pinned tabs differently. The model makes +// sure all pinned tabs are organized at the beginning of the tabstrip. +// Inserting a tab between pinned tabs implicitly makes the inserted tab +// pinned. Similarly moving a tab may pin or unpin the tab, again enforcing +// that all pinned tabs occur at the beginning of the tabstrip. Lastly, +// changing the pinned state of a tab moves the tab to be grouped with the +// pinned or unpinned tabs. For example, if the first two tabs are pinned, and +// the tenth tab is pinned, it is moved to become the third tab. +// . App. An app tab corresponds to an app extension. +// . Phantom. Only pinned app tabs may be made phantom (or if +// browser_defaults::kPinnedTabsActLikeApps is true then any pinned tab may be +// made phantom). When a tab that can be made phantom is closed the renderer +// is shutdown, a new TabContents/NavigationController is created that has +// not yet loaded the renderer and observers are notified via the +// TabReplacedAt method. When a phantom tab is selected the renderer is +// loaded and the tab is no longer phantom. +// Phantom tabs do not prevent the tabstrip from closing, for example if the +// tabstrip has one phantom and one non-phantom tab and the non-phantom tab is +// closed, then the tabstrip/browser are closed. // // A TabStripModel has one delegate that it relies on to perform certain tasks // like creating new TabStripModels (probably hosted in Browser windows) when @@ -267,6 +284,11 @@ class TabStripModel : public NotificationObserver { int count() const { return static_cast<int>(contents_data_.size()); } bool empty() const { return contents_data_.empty(); } + // Returns true if there are any non-phantom tabs. When there are no + // non-phantom tabs the delegate is notified by way of TabStripEmpty and the + // browser closes. + bool HasNonPhantomTabs() const; + // Retrieve the Profile associated with this TabStripModel. Profile* profile() const { return profile_; } @@ -391,12 +413,14 @@ class TabStripModel : public NotificationObserver { // If |use_group| is true, the group property of the tab is used instead of // the opener to find the next tab. Under some circumstances the group // relationship may exist but the opener may not. + // NOTE: this skips phantom tabs. int GetIndexOfNextTabContentsOpenedBy(const NavigationController* opener, int start_index, bool use_group) const; // Returns the index of the last TabContents in the model opened by the // specified opener, starting at |start_index|. + // NOTE: this skips phantom tabs. int GetIndexOfLastTabContentsOpenedBy(const NavigationController* opener, int start_index) const; @@ -434,6 +458,17 @@ class TabStripModel : public NotificationObserver { // Returns true if the tab at |index| is pinned. bool IsTabPinned(int index) const; + // Is the tab at |index| an app? + // See description above class for details on this. + // This is currently only true if browser_defaults::kPinnedTabsActLikeApps is + // true and the tab is pinned. + bool IsAppTab(int index) const; + + // Returns true if the tab is a phantom tab. A phantom tab is one where the + // renderer has not been loaded. + // See description above class for details on this. + bool IsPhantomTab(int index) const; + // Returns true if the tab at |index| is blocked by a tab modal dialog. bool IsTabBlocked(int index) const; @@ -560,6 +595,17 @@ class TabStripModel : public NotificationObserver { // be |opener|'s NavigationController. void SetOpenerForContents(TabContents* contents, TabContents* opener); + // Selects either the next tab (|foward| is true), or the previous tab + // (|forward| is false). + void SelectRelativeTab(bool forward); + + // Returns the first non-phantom tab starting at |index|, skipping the tab at + // |ignore_index|. + int IndexOfNextNonPhantomTab(int index, int ignore_index); + + // Makes the tab a phantom tab. + void MakePhantom(int index); + // Returns true if the tab represented by the specified data has an opener // that matches the specified one. If |use_group| is true, then this will // fall back to check the group relationship as well. diff --git a/chrome/browser/tabs/tab_strip_model_order_controller.cc b/chrome/browser/tabs/tab_strip_model_order_controller.cc index b91c622..de0ab23 100644 --- a/chrome/browser/tabs/tab_strip_model_order_controller.cc +++ b/chrome/browser/tabs/tab_strip_model_order_controller.cc @@ -53,7 +53,8 @@ int TabStripModelOrderController::DetermineInsertionIndex( } int TabStripModelOrderController::DetermineNewSelectedIndex( - int removing_index) const { + int removing_index, + bool is_remove) const { int tab_count = tabstrip_->count(); DCHECK(removing_index >= 0 && removing_index < tab_count); NavigationController* parent_opener = @@ -67,7 +68,7 @@ int TabStripModelOrderController::DetermineNewSelectedIndex( removing_index, false); if (index != TabStripModel::kNoTab) - return GetValidIndex(index, removing_index); + return GetValidIndex(index, removing_index, is_remove); if (parent_opener) { // If the tab was in a group, shift selection to the next tab in the group. @@ -75,19 +76,19 @@ int TabStripModelOrderController::DetermineNewSelectedIndex( removing_index, false); if (index != TabStripModel::kNoTab) - return GetValidIndex(index, removing_index); + return GetValidIndex(index, removing_index, is_remove); // If we can't find a subsequent group member, just fall back to the // parent_opener itself. Note that we use "group" here since opener is // reset by select operations.. index = tabstrip_->GetIndexOfController(parent_opener); if (index != TabStripModel::kNoTab) - return GetValidIndex(index, removing_index); + return GetValidIndex(index, removing_index, is_remove); } // No opener set, fall through to the default handler... int selected_index = tabstrip_->selected_index(); - if (selected_index >= (tab_count - 1)) + if (is_remove && selected_index >= (tab_count - 1)) return selected_index - 1; return selected_index; } @@ -121,8 +122,9 @@ void TabStripModelOrderController::TabSelectedAt(TabContents* old_contents, // TabStripModelOrderController, private: int TabStripModelOrderController::GetValidIndex(int index, - int removing_index) const { - if (removing_index < index) + int removing_index, + bool is_remove) const { + if (is_remove && removing_index < index) index = std::max(0, index - 1); return index; } diff --git a/chrome/browser/tabs/tab_strip_model_order_controller.h b/chrome/browser/tabs/tab_strip_model_order_controller.h index 045b821..9beaa03 100644 --- a/chrome/browser/tabs/tab_strip_model_order_controller.h +++ b/chrome/browser/tabs/tab_strip_model_order_controller.h @@ -29,8 +29,11 @@ class TabStripModelOrderController : public TabStripModelObserver { PageTransition::Type transition, bool foreground); - // Determine where to shift selection after a tab is closed. - int DetermineNewSelectedIndex(int removed_index) const; + // Determine where to shift selection after a tab is closed is made phantom. + // If |is_remove| is false, the tab is not being removed but rather made + // phantom (see description of phantom tabs in TabStripModel). + int DetermineNewSelectedIndex(int removed_index, + bool is_remove) const; // Overridden from TabStripModelObserver: virtual void TabSelectedAt(TabContents* old_contents, @@ -40,9 +43,10 @@ class TabStripModelOrderController : public TabStripModelObserver { protected: // Returns a valid index to be selected after the tab at |removing_index| is - // closed. If |index| is after |removing_index|, |index| is adjusted to - // reflect the fact that |removing_index| is going away. - int GetValidIndex(int index, int removing_index) const; + // closed. If |index| is after |removing_index| and |is_remove| is true, + // |index| is adjusted to reflect the fact that |removing_index| is going + // away. This also skips any phantom tabs. + int GetValidIndex(int index, int removing_index, bool is_remove) const; TabStripModel* tabstrip_; }; diff --git a/chrome/browser/views/frame/browser_view.cc b/chrome/browser/views/frame/browser_view.cc index ca001a3..f0a595c 100644 --- a/chrome/browser/views/frame/browser_view.cc +++ b/chrome/browser/views/frame/browser_view.cc @@ -1475,7 +1475,7 @@ bool BrowserView::CanClose() const { if (!browser_->ShouldCloseWindow()) return false; - if (!browser_->tabstrip_model()->empty()) { + if (browser_->tabstrip_model()->HasNonPhantomTabs()) { // Tab strip isn't empty. Hide the frame (so it appears to have closed // immediately) and close all the tabs, allowing the renderers to shut // down. When the tab strip is empty we'll be called back again. diff --git a/chrome/browser/views/tabs/dragged_tab_controller.cc b/chrome/browser/views/tabs/dragged_tab_controller.cc index f7f81b8..e354e56 100644 --- a/chrome/browser/views/tabs/dragged_tab_controller.cc +++ b/chrome/browser/views/tabs/dragged_tab_controller.cc @@ -959,7 +959,7 @@ void DraggedTabController::Detach() { } // If we've removed the last Tab from the TabStrip, hide the frame now. - if (attached_model->empty()) + if (!attached_model->HasNonPhantomTabs()) HideFrame(); // Set up the photo booth to start capturing the contents of the dragged @@ -1343,7 +1343,7 @@ void DraggedTabController::HideFrame() { void DraggedTabController::CleanUpHiddenFrame() { // If the model we started dragging from is now empty, we must ask the // delegate to close the frame. - if (source_tabstrip_->model()->empty()) + if (!source_tabstrip_->model()->HasNonPhantomTabs()) source_tabstrip_->model()->delegate()->CloseFrameAfterDragSession(); } diff --git a/chrome/browser/views/tabs/dragged_tab_view.cc b/chrome/browser/views/tabs/dragged_tab_view.cc index d3a9263..5cd7531 100644 --- a/chrome/browser/views/tabs/dragged_tab_view.cc +++ b/chrome/browser/views/tabs/dragged_tab_view.cc @@ -42,7 +42,7 @@ DraggedTabView::DraggedTabView(TabContents* datasource, tab_width_(0) { set_parent_owned(false); - renderer_->UpdateData(datasource, false); + renderer_->UpdateData(datasource, false, false); #if defined(OS_WIN) container_.reset(new views::WidgetWin); diff --git a/chrome/browser/views/tabs/tab_renderer.cc b/chrome/browser/views/tabs/tab_renderer.cc index 60d24c9..34bee57 100644 --- a/chrome/browser/views/tabs/tab_renderer.cc +++ b/chrome/browser/views/tabs/tab_renderer.cc @@ -258,6 +258,7 @@ TabRenderer::TabRenderer() data_.blocked = false; data_.pinned = false; data_.animating_pinned_change = false; + data_.phantom = false; // Add the Close Button. close_button_ = new TabCloseButton(this); @@ -293,13 +294,16 @@ ThemeProvider* TabRenderer::GetThemeProvider() { return NULL; } -void TabRenderer::UpdateData(TabContents* contents, bool loading_only) { +void TabRenderer::UpdateData(TabContents* contents, + bool phantom, + bool loading_only) { DCHECK(contents); - if (!loading_only) { + if (data_.phantom != phantom || !loading_only) { data_.title = contents->GetTitle(); data_.off_the_record = contents->profile()->IsOffTheRecord(); data_.crashed = contents->is_crashed(); data_.favicon = contents->GetFavIcon(); + data_.phantom = phantom; } // TODO(glen): Temporary hax. @@ -457,7 +461,8 @@ void TabRenderer::Paint(gfx::Canvas* canvas) { show_close_button != showing_close_button_) Layout(); - PaintTabBackground(canvas); + if (!data_.phantom) + PaintTabBackground(canvas); SkColor title_color = GetThemeProvider()-> GetColor(IsSelected() ? diff --git a/chrome/browser/views/tabs/tab_renderer.h b/chrome/browser/views/tabs/tab_renderer.h index 4cecfbe..c08dd8a 100644 --- a/chrome/browser/views/tabs/tab_renderer.h +++ b/chrome/browser/views/tabs/tab_renderer.h @@ -45,7 +45,7 @@ class TabRenderer : public views::View, // TabContents. // // See TabStripModel::TabChangedAt documentation for what loading_only means. - void UpdateData(TabContents* contents, bool loading_only); + void UpdateData(TabContents* contents, bool phantom, bool loading_only); // Sets the pinned state of the tab. void SetBlocked(bool blocked); @@ -207,6 +207,7 @@ class TabRenderer : public views::View, bool pinned; bool blocked; bool animating_pinned_change; + bool phantom; }; TabData data_; diff --git a/chrome/browser/views/tabs/tab_strip.cc b/chrome/browser/views/tabs/tab_strip.cc index 27bbb68..e051f7d 100644 --- a/chrome/browser/views/tabs/tab_strip.cc +++ b/chrome/browser/views/tabs/tab_strip.cc @@ -1111,7 +1111,7 @@ void TabStrip::TabInsertedAt(TabContents* contents, if (!contains_tab) { TabData d = { tab, gfx::Rect() }; tab_data_.insert(tab_data_.begin() + index, d); - tab->UpdateData(contents, false); + tab->UpdateData(contents, model_->IsPhantomTab(index), false); } tab->set_pinned(model_->IsTabPinned(index)); tab->SetBlocked(model_->IsTabBlocked(index)); @@ -1186,10 +1186,17 @@ void TabStrip::TabChangedAt(TabContents* contents, int index, // We'll receive another notification of the change asynchronously. return; } - tab->UpdateData(contents, change_type == LOADING_ONLY); + tab->UpdateData(contents, model_->IsPhantomTab(index), + change_type == LOADING_ONLY); tab->UpdateFromModel(); } +void TabStrip::TabReplacedAt(TabContents* old_contents, + TabContents* new_contents, + int index) { + TabChangedAt(new_contents, index, ALL); +} + void TabStrip::TabPinnedStateChanged(TabContents* contents, int index) { GetTabAt(index)->set_pinned(model_->IsTabPinned(index)); StartPinnedTabAnimation(index); diff --git a/chrome/browser/views/tabs/tab_strip.h b/chrome/browser/views/tabs/tab_strip.h index 27f6a97..309a9f3 100644 --- a/chrome/browser/views/tabs/tab_strip.h +++ b/chrome/browser/views/tabs/tab_strip.h @@ -139,6 +139,9 @@ class TabStrip : public views::View, bool pinned_state_changed); virtual void TabChangedAt(TabContents* contents, int index, TabChangeType change_type); + virtual void TabReplacedAt(TabContents* old_contents, + TabContents* new_contents, + int index); virtual void TabPinnedStateChanged(TabContents* contents, int index); virtual void TabBlockedStateChanged(TabContents* contents, int index); diff --git a/chrome/common/chrome_switches.cc b/chrome/common/chrome_switches.cc index 251b382..e762c36 100644 --- a/chrome/common/chrome_switches.cc +++ b/chrome/common/chrome_switches.cc @@ -228,6 +228,9 @@ const char kEnableNativeWebWorkers[] = "enable-native-web-workers"; // Enable AutoFill++. const char kEnableNewAutoFill[] = "enable-new-autofill"; +// Are phantom tabs enabled? +const char kEnablePhantomTabs[] = "enable-phantom-tabs"; + // Enable Privacy Blacklists. const char kEnablePrivacyBlacklists[] = "enable-privacy-blacklists"; diff --git a/chrome/common/chrome_switches.h b/chrome/common/chrome_switches.h index 15b1975..a4a7c69 100644 --- a/chrome/common/chrome_switches.h +++ b/chrome/common/chrome_switches.h @@ -81,6 +81,7 @@ extern const char kEnableLogging[]; extern const char kEnableMonitorProfile[]; extern const char kEnableNativeWebWorkers[]; extern const char kEnableNewAutoFill[]; +extern const char kEnablePhantomTabs[]; extern const char kEnablePrivacyBlacklists[]; extern const char kEnableRendererAccessibility[]; extern const char kEnableSeccompSandbox[]; |