| // Copyright (c) 2006-2008 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 <algorithm> |
| |
| #include "base/gfx/point.h" |
| #include "base/logging.h" |
| #include "chrome/browser/browser.h" |
| #include "chrome/browser/browser_about_handler.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/dom_ui/new_tab_ui.h" |
| #include "chrome/browser/profile.h" |
| #include "chrome/browser/navigation_controller.h" |
| #include "chrome/browser/navigation_entry.h" |
| #include "chrome/browser/render_view_host.h" |
| #include "chrome/browser/tab_contents_factory.h" |
| #include "chrome/browser/tab_restore_service.h" |
| #include "chrome/browser/tabs/tab_strip_model.h" |
| #include "chrome/browser/tabs/tab_strip_model_order_controller.h" |
| #include "chrome/browser/user_metrics.h" |
| #include "chrome/common/notification_service.h" |
| #include "chrome/common/pref_names.h" |
| #include "chrome/common/pref_service.h" |
| #include "chrome/common/stl_util-inl.h" |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // TabStripModel, public: |
| |
| TabStripModel::TabStripModel(TabStripModelDelegate* delegate, Profile* profile) |
| : delegate_(delegate), |
| profile_(profile), |
| selected_index_(kNoTab), |
| closing_all_(false), |
| order_controller_(NULL) { |
| NotificationService::current()->AddObserver(this, |
| NOTIFY_TAB_CONTENTS_DESTROYED, NotificationService::AllSources()); |
| SetOrderController(new TabStripModelOrderController(this)); |
| } |
| |
| TabStripModel::~TabStripModel() { |
| STLDeleteContainerPointers(contents_data_.begin(), contents_data_.end()); |
| delete order_controller_; |
| NotificationService::current()->RemoveObserver(this, |
| NOTIFY_TAB_CONTENTS_DESTROYED, NotificationService::AllSources()); |
| } |
| |
| void TabStripModel::AddObserver(TabStripModelObserver* observer) { |
| observers_.AddObserver(observer); |
| } |
| |
| void TabStripModel::RemoveObserver(TabStripModelObserver* observer) { |
| observers_.RemoveObserver(observer); |
| } |
| |
| void TabStripModel::SetOrderController( |
| TabStripModelOrderController* order_controller) { |
| if (order_controller_) |
| delete order_controller_; |
| order_controller_ = order_controller; |
| } |
| |
| bool TabStripModel::ContainsIndex(int index) const { |
| return index >= 0 && index < count(); |
| } |
| |
| void TabStripModel::AppendTabContents(TabContents* contents, bool foreground) { |
| // Tabs opened in the foreground using this method inherit the group of the |
| // previously selected tab. |
| InsertTabContentsAt(count(), contents, foreground, foreground); |
| } |
| |
| void TabStripModel::InsertTabContentsAt(int index, |
| TabContents* contents, |
| bool foreground, |
| bool inherit_group) { |
| // In tab dragging situations, if the last tab in the window was detached |
| // then the user aborted the drag, we will have the |closing_all_| member |
| // set (see DetachTabContentsAt) which will mess with our mojo here. We need |
| // to clear this bit. |
| closing_all_ = false; |
| |
| // Have to get the selected contents before we monkey with |contents_| |
| // otherwise we run into problems when we try to change the selected contents |
| // since the old contents and the new contents will be the same... |
| TabContents* selected_contents = GetSelectedTabContents(); |
| TabContentsData* data = new TabContentsData(contents); |
| if (inherit_group && selected_contents) { |
| if (foreground) { |
| // Forget any existing relationships, we don't want to make things too |
| // confusing by having multiple groups active at the same time. |
| ForgetAllOpeners(); |
| } |
| // Anything opened by a link we deem to have an opener. |
| data->SetGroup(selected_contents->controller()); |
| } |
| contents_data_.insert(contents_data_.begin() + index, data); |
| |
| FOR_EACH_OBSERVER(TabStripModelObserver, observers_, |
| TabInsertedAt(contents, index, foreground)); |
| |
| if (foreground) |
| ChangeSelectedContentsFrom(selected_contents, index, false); |
| } |
| |
| void TabStripModel::ReplaceNavigationControllerAt( |
| int index, NavigationController* controller) { |
| // This appears to be OK with no flicker since no redraw event |
| // occurs between the call to add an aditional tab and one to close |
| // the previous tab. |
| InsertTabContentsAt(index + 1, controller->active_contents(), true, true); |
| InternalCloseTabContentsAt(index, false); |
| } |
| |
| TabContents* TabStripModel::DetachTabContentsAt(int index) { |
| if (contents_data_.empty()) |
| return NULL; |
| |
| DCHECK(ContainsIndex(index)); |
| TabContents* removed_contents = GetContentsAt(index); |
| next_selected_index_ = order_controller_->DetermineNewSelectedIndex(index); |
| delete contents_data_.at(index); |
| contents_data_.erase(contents_data_.begin() + index); |
| if (contents_data_.empty()) |
| closing_all_ = true; |
| TabStripModelObservers::Iterator iter(observers_); |
| while (TabStripModelObserver* obs = iter.GetNext()) { |
| obs->TabDetachedAt(removed_contents, index); |
| if (empty()) |
| obs->TabStripEmpty(); |
| } |
| if (!contents_data_.empty()) { |
| if (index == selected_index_) { |
| ChangeSelectedContentsFrom(removed_contents, next_selected_index_, |
| false); |
| } else if (index < selected_index_) { |
| // If the removed tab was before the selected index, we need to account |
| // for this in the selected index... |
| SelectTabContentsAt(selected_index_ - 1, false); |
| } |
| } |
| next_selected_index_ = selected_index_; |
| return removed_contents; |
| } |
| |
| void TabStripModel::SelectTabContentsAt(int index, bool user_gesture) { |
| DCHECK(ContainsIndex(index)); |
| ChangeSelectedContentsFrom(GetSelectedTabContents(), index, user_gesture); |
| } |
| |
| void TabStripModel::ReplaceTabContentsAt(int index, |
| TabContents* replacement_contents) { |
| DCHECK(ContainsIndex(index)); |
| TabContents* old_contents = GetContentsAt(index); |
| contents_data_[index]->contents = replacement_contents; |
| |
| FOR_EACH_OBSERVER(TabStripModelObserver, observers_, |
| TabChangedAt(replacement_contents, index)); |
| |
| // Re-use the logic for selecting tabs to ensure the replacement contents is |
| // shown and sized appropriately. |
| if (index == selected_index_) { |
| FOR_EACH_OBSERVER(TabStripModelObserver, observers_, |
| TabSelectedAt(old_contents, replacement_contents, index, false)); |
| } |
| } |
| |
| void TabStripModel::MoveTabContentsAt(int index, int to_position) { |
| DCHECK(ContainsIndex(index)); |
| if (index == to_position) |
| return; |
| |
| TabContentsData* moved_data = contents_data_.at(index); |
| contents_data_.erase(contents_data_.begin() + index); |
| contents_data_.insert(contents_data_.begin() + to_position, moved_data); |
| |
| selected_index_ = to_position; |
| |
| FOR_EACH_OBSERVER(TabStripModelObserver, observers_, |
| TabMoved(moved_data->contents, index, to_position)); |
| } |
| |
| TabContents* TabStripModel::GetSelectedTabContents() const { |
| return GetTabContentsAt(selected_index_); |
| } |
| |
| TabContents* TabStripModel::GetTabContentsAt(int index) const { |
| if (ContainsIndex(index)) |
| return GetContentsAt(index); |
| return NULL; |
| } |
| |
| int TabStripModel::GetIndexOfTabContents(const TabContents* contents) const { |
| int index = 0; |
| TabContentsDataVector::const_iterator iter = contents_data_.begin(); |
| for (; iter != contents_data_.end(); ++iter, ++index) { |
| if ((*iter)->contents == contents) |
| return index; |
| } |
| return kNoTab; |
| } |
| |
| int TabStripModel::GetIndexOfController( |
| const NavigationController* controller) const { |
| int index = 0; |
| TabContentsDataVector::const_iterator iter = contents_data_.begin(); |
| for (; iter != contents_data_.end(); ++iter, ++index) { |
| if ((*iter)->contents->controller() == controller) |
| return index; |
| } |
| return kNoTab; |
| } |
| |
| void TabStripModel::UpdateTabContentsStateAt(int index) { |
| DCHECK(ContainsIndex(index)); |
| |
| FOR_EACH_OBSERVER(TabStripModelObserver, observers_, |
| TabChangedAt(GetContentsAt(index), index)); |
| } |
| |
| void TabStripModel::UpdateTabContentsLoadingAnimations() { |
| FOR_EACH_OBSERVER(TabStripModelObserver, observers_, |
| TabValidateAnimations()); |
| } |
| |
| void TabStripModel::CloseAllTabs() { |
| // Set state so that observers can adjust their behavior to suit this |
| // specific condition when CloseTabContentsAt causes a flurry of |
| // Close/Detach/Select notifications to be sent. |
| closing_all_ = true; |
| for (int i = count() - 1; i >= 0; --i) |
| CloseTabContentsAt(i); |
| } |
| |
| bool TabStripModel::TabsAreLoading() const { |
| TabContentsDataVector::const_iterator iter = contents_data_.begin(); |
| for (; iter != contents_data_.end(); ++iter) { |
| if ((*iter)->contents->is_loading()) |
| return true; |
| } |
| return false; |
| } |
| |
| bool TabStripModel::TabHasUnloadListener(int index) { |
| // TODO(beng): this should call through to the delegate, so we can mock it |
| // in testing and then provide better test coverage for features |
| // like "close other tabs". |
| WebContents* web_contents = GetContentsAt(index)->AsWebContents(); |
| if (web_contents) { |
| // If the WebContents is not connected yet, then there's no unload |
| // handler we can fire even if the WebContents has an unload listener. |
| // One case where we hit this is in a tab that has an infinite loop |
| // before load. |
| return web_contents->notify_disconnection() && |
| !web_contents->showing_interstitial_page() && |
| web_contents->render_view_host()->HasUnloadListener(); |
| } |
| return false; |
| } |
| |
| NavigationController* TabStripModel::GetOpenerOfTabContentsAt(int index) { |
| DCHECK(ContainsIndex(index)); |
| return contents_data_.at(index)->opener; |
| } |
| |
| int TabStripModel::GetIndexOfNextTabContentsOpenedBy( |
| NavigationController* opener, int start_index, bool use_group) { |
| 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); |
| 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)) |
| return static_cast<int>(next - contents_data_.begin()); |
| } |
| iter = find(contents_data_.begin(), contents_data_.end(), start_data); |
| if (iter != contents_data_.begin()) { |
| for (--iter; iter > contents_data_.begin(); --iter) { |
| if (OpenerMatches(*iter, opener, use_group)) |
| return static_cast<int>(iter - contents_data_.begin()); |
| } |
| } |
| return kNoTab; |
| } |
| |
| int TabStripModel::GetIndexOfLastTabContentsOpenedBy( |
| NavigationController* opener, int start_index) { |
| 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(); |
| TabContentsDataVector::const_iterator next; |
| for (; iter != end; --iter) { |
| next = iter - 1; |
| if (next == end) |
| break; |
| if ((*next)->opener == opener) |
| return static_cast<int>(next - contents_data_.begin()); |
| } |
| return kNoTab; |
| } |
| |
| void TabStripModel::ForgetAllOpeners() { |
| // Forget all opener memories so we don't do anything weird with tab |
| // re-selection ordering. |
| TabContentsDataVector::const_iterator iter = contents_data_.begin(); |
| for (; iter != contents_data_.end(); ++iter) |
| (*iter)->ForgetOpener(); |
| } |
| |
| void TabStripModel::ForgetGroup(TabContents* contents) { |
| int index = GetIndexOfTabContents(contents); |
| DCHECK(ContainsIndex(index)); |
| contents_data_.at(index)->SetGroup(NULL); |
| contents_data_.at(index)->ForgetOpener(); |
| } |
| |
| bool TabStripModel::ShouldResetGroupOnSelect(TabContents* contents) const { |
| int index = GetIndexOfTabContents(contents); |
| DCHECK(ContainsIndex(index)); |
| return contents_data_.at(index)->reset_group_on_select; |
| } |
| |
| TabContents* TabStripModel::AddBlankTab(bool foreground) { |
| DCHECK(delegate_); |
| TabContents* contents = delegate_->CreateTabContentsForURL( |
| NewTabUIURL(), GURL(), profile_, PageTransition::TYPED, false, NULL); |
| AddTabContents(contents, -1, PageTransition::TYPED, foreground); |
| return contents; |
| } |
| |
| TabContents* TabStripModel::AddBlankTabAt(int index, bool foreground) { |
| DCHECK(delegate_); |
| TabContents* contents = delegate_->CreateTabContentsForURL( |
| NewTabUIURL(), GURL(), profile_, PageTransition::LINK, false, NULL); |
| AddTabContents(contents, index, PageTransition::LINK, foreground); |
| return contents; |
| } |
| |
| void TabStripModel::AddTabContents(TabContents* contents, |
| int index, |
| PageTransition::Type transition, |
| bool foreground) { |
| if (transition == PageTransition::LINK) { |
| // Only try to be clever if we're opening a LINK. |
| index = order_controller_->DetermineInsertionIndex( |
| contents, transition, foreground); |
| } else { |
| // For all other types, respect what was passed to us, normalizing -1s. |
| if (index < 0) |
| index = count(); |
| } |
| TabContents* last_selected_contents = GetSelectedTabContents(); |
| // Tabs opened from links inherit the "group" attribute of the Tab from which |
| // they were opened. This means when they're closed, that Tab will be |
| // selected again. |
| bool inherit_group = transition == PageTransition::LINK; |
| if (!inherit_group) { |
| // Also, any tab opened at the end of the TabStrip with a "TYPED" |
| // transition inherit group as well. This covers the cases where the user |
| // creates a New Tab (e.g. Ctrl+T, or clicks the New Tab button), or types |
| // in the address bar and presses Alt+Enter. This allows for opening a new |
| // Tab to quickly look up something. When this Tab is closed, the old one |
| // is re-selected, not the next-adjacent. |
| inherit_group = transition == PageTransition::TYPED && index == count(); |
| } |
| InsertTabContentsAt(index, contents, foreground, inherit_group); |
| if (inherit_group && transition == PageTransition::TYPED) |
| contents_data_.at(index)->reset_group_on_select = true; |
| } |
| |
| void TabStripModel::CloseSelectedTab() { |
| CloseTabContentsAt(selected_index_); |
| } |
| |
| 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); |
| } |
| |
| void TabStripModel::SelectPreviousTab() { |
| int prev_index = selected_index_ - 1; |
| if (prev_index < 0) |
| prev_index = count() + prev_index; |
| SelectTabContentsAt(prev_index, true); |
| } |
| |
| void TabStripModel::SelectLastTab() { |
| SelectTabContentsAt(count() - 1, true); |
| } |
| |
| void TabStripModel::TearOffTabContents(TabContents* detached_contents, |
| const gfx::Point& drop_point) { |
| DCHECK(detached_contents); |
| delegate_->CreateNewStripWithContents(detached_contents, drop_point); |
| } |
| |
| // Context menu functions. |
| bool TabStripModel::IsContextMenuCommandEnabled( |
| int context_index, ContextMenuCommand command_id) { |
| DCHECK(command_id > CommandFirst && command_id < CommandLast); |
| switch (command_id) { |
| case CommandNewTab: |
| case CommandReload: |
| case CommandCloseTab: |
| return true; |
| case CommandCloseOtherTabs: |
| return count() > 1; |
| case CommandCloseTabsToRight: |
| return context_index < (count() - 1); |
| case CommandCloseTabsOpenedBy: { |
| NavigationController* opener = |
| GetTabContentsAt(context_index)->controller(); |
| int next_index = |
| GetIndexOfNextTabContentsOpenedBy(opener, context_index, true); |
| return next_index != kNoTab; |
| } |
| case CommandDuplicate: |
| if (delegate_) |
| return delegate_->CanDuplicateContentsAt(context_index); |
| else |
| return false; |
| default: |
| NOTREACHED(); |
| } |
| return false; |
| } |
| |
| void TabStripModel::ExecuteContextMenuCommand( |
| int context_index, ContextMenuCommand command_id) { |
| DCHECK(command_id > CommandFirst && command_id < CommandLast); |
| switch (command_id) { |
| case CommandNewTab: |
| UserMetrics::RecordAction(L"TabContextMenu_NewTab", profile_); |
| AddBlankTabAt(context_index + 1, true); |
| break; |
| case CommandReload: |
| UserMetrics::RecordAction(L"TabContextMenu_Reload", profile_); |
| GetContentsAt(context_index)->controller()->Reload(true); |
| break; |
| case CommandDuplicate: |
| if (delegate_) { |
| UserMetrics::RecordAction(L"TabContextMenu_Duplicate", profile_); |
| delegate_->DuplicateContentsAt(context_index); |
| } |
| break; |
| case CommandCloseTab: |
| UserMetrics::RecordAction(L"TabContextMenu_CloseTab", profile_); |
| CloseTabContentsAt(context_index); |
| break; |
| case CommandCloseOtherTabs: { |
| UserMetrics::RecordAction(L"TabContextMenu_CloseOtherTabs", profile_); |
| TabContents* contents = GetTabContentsAt(context_index); |
| for (int i = count() - 1; i >= 0; --i) { |
| if (GetTabContentsAt(i) != contents) |
| CloseTabContentsAt(i); |
| } |
| break; |
| } |
| case CommandCloseTabsToRight: { |
| UserMetrics::RecordAction(L"TabContextMenu_CloseTabsToRight", profile_); |
| for (int i = count() - 1; i > context_index; --i) |
| CloseTabContentsAt(i); |
| break; |
| } |
| case CommandCloseTabsOpenedBy: { |
| UserMetrics::RecordAction(L"TabContextMenu_CloseTabsOpenedBy", profile_); |
| NavigationController* opener = |
| GetTabContentsAt(context_index)->controller(); |
| |
| for (int i = count() - 1; i >= 0; --i) { |
| if (OpenerMatches(contents_data_.at(i), opener, true)) |
| CloseTabContentsAt(i); |
| } |
| |
| break; |
| } |
| default: |
| NOTREACHED(); |
| } |
| } |
| |
| std::vector<int> TabStripModel::GetIndexesOpenedBy(int index) const { |
| std::vector<int> indices; |
| NavigationController* opener = GetTabContentsAt(index)->controller(); |
| for (int i = count() - 1; i >= 0; --i) { |
| if (OpenerMatches(contents_data_.at(i), opener, true)) |
| indices.push_back(i); |
| } |
| return indices; |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // TabStripModel, NotificationObserver implementation: |
| |
| void TabStripModel::Observe(NotificationType type, |
| const NotificationSource& source, |
| const NotificationDetails& details) { |
| DCHECK(type == NOTIFY_TAB_CONTENTS_DESTROYED); |
| // Sometimes, on qemu, it seems like a TabContents object can be destroyed |
| // while we still have a reference to it. We need to break this reference |
| // here so we don't crash later. |
| int index = GetIndexOfTabContents(Source<TabContents>(source).ptr()); |
| 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); |
| } |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // TabStripModel, private: |
| |
| bool TabStripModel::InternalCloseTabContentsAt(int index, |
| bool create_historical_tab) { |
| TabContents* detached_contents = GetContentsAt(index); |
| |
| if (TabHasUnloadListener(index)) { |
| // If the page has unload listeners, then we tell the renderer to fire |
| // them. Once they have fired, we'll get a message back saying whether |
| // to proceed closing the page or not, which sends us back to this method |
| // with the HasUnloadListener bit cleared. |
| WebContents* web_contents = GetContentsAt(index)->AsWebContents(); |
| // If we hit this code path, the tab had better be a WebContents tab. |
| DCHECK(web_contents); |
| web_contents->render_view_host()->FirePageBeforeUnload(); |
| return false; |
| } |
| |
| // TODO: Now that we know the tab has no unload/beforeunload listeners, |
| // we should be able to do a fast shutdown of the RenderViewProcess. |
| // Make sure that we actually do. |
| |
| FOR_EACH_OBSERVER(TabStripModelObserver, observers_, |
| TabClosingAt(detached_contents, index)); |
| |
| const bool add_to_restore_service = |
| (detached_contents && create_historical_tab && |
| ShouldAddToTabRestoreService(detached_contents)); |
| if (detached_contents) { |
| if (add_to_restore_service) { |
| profile()->GetTabRestoreService()-> |
| CreateHistoricalTab(detached_contents->controller()); |
| } |
| detached_contents->CloseContents(); |
| // Closing the TabContents will later call back to us via |
| // NotificationObserver and detach it. |
| } |
| return true; |
| } |
| |
| TabContents* TabStripModel::GetContentsAt(int index) const { |
| CHECK(ContainsIndex(index)) << |
| "Failed to find: " << index << " in: " << count() << " entries."; |
| return contents_data_.at(index)->contents; |
| } |
| |
| void TabStripModel::ChangeSelectedContentsFrom( |
| TabContents* old_contents, int to_index, bool user_gesture) { |
| DCHECK(ContainsIndex(to_index)); |
| TabContents* new_contents = GetContentsAt(to_index); |
| if (old_contents == new_contents) |
| return; |
| TabContents* last_selected_contents = old_contents; |
| int from_index = selected_index_; |
| selected_index_ = to_index; |
| |
| FOR_EACH_OBSERVER(TabStripModelObserver, observers_, |
| TabSelectedAt(last_selected_contents, new_contents, selected_index_, |
| user_gesture)); |
| } |
| |
| void TabStripModel::SetOpenerForContents(TabContents* contents, |
| TabContents* opener) { |
| int index = GetIndexOfTabContents(contents); |
| contents_data_.at(index)->opener = opener->controller(); |
| } |
| |
| bool TabStripModel::ShouldAddToTabRestoreService(TabContents* contents) { |
| if (!profile() || profile()->IsOffTheRecord() || |
| !profile()->GetTabRestoreService()) { |
| return false; |
| } |
| |
| Browser* browser = |
| Browser::GetBrowserForController(contents->controller(), NULL); |
| if (!browser) |
| return false; // Browser is null during unit tests. |
| return browser->GetType() == BrowserType::TABBED_BROWSER; |
| } |
| |
| // static |
| bool TabStripModel::OpenerMatches(TabContentsData* data, |
| NavigationController* opener, |
| bool use_group) { |
| return data->opener == opener || (use_group && data->group == opener); |
| } |
| |
| |