diff options
author | sky@chromium.org <sky@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-07-08 20:39:46 +0000 |
---|---|---|
committer | sky@chromium.org <sky@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-07-08 20:39:46 +0000 |
commit | 8f327a0086540250d2af9454c2138bbe514a3aea (patch) | |
tree | c73ef8418a1b42069156b1c83e2d494845c59408 /chrome/browser/tabs | |
parent | ce185864d3ef3d2a3fd40ae93300a5858f961c94 (diff) | |
download | chromium_src-8f327a0086540250d2af9454c2138bbe514a3aea.zip chromium_src-8f327a0086540250d2af9454c2138bbe514a3aea.tar.gz chromium_src-8f327a0086540250d2af9454c2138bbe514a3aea.tar.bz2 |
Adds tab pinning to TabStripModel. This is just the model side of
things (and tests), no UI yet.
The model enforces that all pinned tabs occur at the beginning of the
model. For example, if there are no pinned tabs and you pin the 10th
tab, it is moved to the front of the tab strip. Similarly inserting a
tab before the last pinned tab implicitly pins it. Moving a pinned tab
beyond the set of pinned tabs implicitly unpins it.
I'll file a bug on this when the site allows me to.
BUG=none
TEST=none
Review URL: http://codereview.chromium.org/155228
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@20181 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser/tabs')
-rw-r--r-- | chrome/browser/tabs/tab_strip_model.cc | 120 | ||||
-rw-r--r-- | chrome/browser/tabs/tab_strip_model.h | 89 | ||||
-rw-r--r-- | chrome/browser/tabs/tab_strip_model_order_controller.cc | 6 | ||||
-rw-r--r-- | chrome/browser/tabs/tab_strip_model_unittest.cc | 256 |
4 files changed, 429 insertions, 42 deletions
diff --git a/chrome/browser/tabs/tab_strip_model.cc b/chrome/browser/tabs/tab_strip_model.cc index 318fb64..5f53a44 100644 --- a/chrome/browser/tabs/tab_strip_model.cc +++ b/chrome/browser/tabs/tab_strip_model.cc @@ -6,6 +6,7 @@ #include <algorithm> +#include "base/command_line.h" #include "base/stl_util-inl.h" #include "base/string_util.h" #include "build/build_config.h" @@ -15,6 +16,7 @@ #include "chrome/browser/tabs/tab_strip_model_order_controller.h" #include "chrome/browser/tab_contents/navigation_controller.h" #include "chrome/browser/tab_contents/tab_contents.h" +#include "chrome/common/chrome_switches.h" #include "chrome/common/notification_service.h" #include "chrome/common/url_constants.h" @@ -89,6 +91,7 @@ void TabStripModel::InsertTabContentsAt(int index, // since the old contents and the new contents will be the same... TabContents* selected_contents = GetSelectedTabContents(); TabContentsData* data = new TabContentsData(contents); + data->pinned = (index != count() && index < IndexOfFirstNonPinnedTab()); if (inherit_group && selected_contents) { if (foreground) { // Forget any existing relationships, we don't want to make things too @@ -159,25 +162,7 @@ void TabStripModel::SelectTabContentsAt(int index, bool user_gesture) { void TabStripModel::MoveTabContentsAt(int index, int to_position, bool select_after_move) { - 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); - - // if !select_after_move, keep the same tab selected as was selected before. - if (select_after_move || index == selected_index_) { - selected_index_ = to_position; - } else if (index < selected_index_ && to_position >= selected_index_) { - selected_index_--; - } else if (index > selected_index_ && to_position <= selected_index_) { - selected_index_++; - } - - FOR_EACH_OBSERVER(TabStripModelObserver, observers_, - TabMoved(moved_data->contents, index, to_position)); + MoveTabContentsAtImpl(index, to_position, select_after_move, true); } TabContents* TabStripModel::GetSelectedTabContents() const { @@ -331,6 +316,57 @@ bool TabStripModel::ShouldResetGroupOnSelect(TabContents* contents) const { return contents_data_.at(index)->reset_group_on_select; } +void TabStripModel::SetTabPinned(int index, bool pinned) { + DCHECK(ContainsIndex(index)); + if (contents_data_[index]->pinned == pinned) + return; + + int first_non_pinned_tab = IndexOfFirstNonPinnedTab(); + + contents_data_[index]->pinned = pinned; + + if (pinned && index > first_non_pinned_tab) { + // The tab is being pinned but beyond the pinned tabs. Move it to the end + // of the pinned tabs. + MoveTabContentsAtImpl(index, first_non_pinned_tab, + selected_index() == index, false); + } else if (!pinned && index < first_non_pinned_tab - 1) { + // The tab is being unpinned, but is within the pinned tabs, move it to + // be after the set of pinned tabs. + MoveTabContentsAtImpl(index, first_non_pinned_tab - 1, + selected_index() == index, false); + } else { + // Tab didn't move, but it's pinned state changed. Notify observers. + FOR_EACH_OBSERVER(TabStripModelObserver, observers_, + TabPinnedStateChanged(contents_data_[index]->contents, + index)); + } +} + +bool TabStripModel::IsTabPinned(int index) const { + return contents_data_[index]->pinned; +} + +int TabStripModel::IndexOfFirstNonPinnedTab() const { + for (size_t i = 0; i < contents_data_.size(); ++i) { + if (!contents_data_[i]->pinned) + return static_cast<int>(i); + } + // No pinned tabs. + return count(); +} + +// static +bool TabStripModel::IsTabPinningEnabled() { + static bool checked = false; + static bool enabled = false; + if (!checked) { + enabled = CommandLine::ForCurrentProcess()->HasSwitch( + switches::kEnableTabPinning); + } + return enabled; +} + void TabStripModel::AddTabContents(TabContents* contents, int index, bool force_index, @@ -419,6 +455,8 @@ bool TabStripModel::IsContextMenuCommandEnabled( return delegate_->CanDuplicateContentsAt(context_index); case CommandRestoreTab: return delegate_->CanRestoreTab(); + case CommandTogglePinned: + return true; default: NOTREACHED(); } @@ -477,6 +515,13 @@ void TabStripModel::ExecuteContextMenuCommand( delegate_->RestoreTab(); break; } + case CommandTogglePinned: { + UserMetrics::RecordAction(L"TabContextMenu_TogglePinned", profile_); + + SelectTabContentsAt(context_index, true); + SetTabPinned(context_index, !IsTabPinned(context_index)); + break; + } default: NOTREACHED(); } @@ -550,6 +595,43 @@ bool TabStripModel::InternalCloseTabContentsAt(int index, return true; } +void TabStripModel::MoveTabContentsAtImpl(int index, int to_position, + bool select_after_move, + bool update_pinned_state) { + DCHECK(ContainsIndex(index)); + if (index == to_position) + return; + + bool pinned_state_changed = !update_pinned_state; + + if (update_pinned_state) { + bool is_pinned = IsTabPinned(index); + if (is_pinned && to_position >= IndexOfFirstNonPinnedTab()) { + contents_data_[index]->pinned = false; + } else if (!is_pinned && to_position < IndexOfFirstNonPinnedTab()) { + contents_data_[index]->pinned = true; + } + pinned_state_changed = is_pinned != contents_data_[index]->pinned; + } + + TabContentsData* moved_data = contents_data_.at(index); + contents_data_.erase(contents_data_.begin() + index); + contents_data_.insert(contents_data_.begin() + to_position, moved_data); + + // if !select_after_move, keep the same tab selected as was selected before. + if (select_after_move || index == selected_index_) { + selected_index_ = to_position; + } else if (index < selected_index_ && to_position >= selected_index_) { + selected_index_--; + } else if (index > selected_index_ && to_position <= selected_index_) { + selected_index_++; + } + + FOR_EACH_OBSERVER(TabStripModelObserver, observers_, + TabMoved(moved_data->contents, index, to_position, + pinned_state_changed)); +} + TabContents* TabStripModel::GetContentsAt(int index) const { CHECK(ContainsIndex(index)) << "Failed to find: " << index << " in: " << count() << " entries."; diff --git a/chrome/browser/tabs/tab_strip_model.h b/chrome/browser/tabs/tab_strip_model.h index fdbe77e..53ff04f 100644 --- a/chrome/browser/tabs/tab_strip_model.h +++ b/chrome/browser/tabs/tab_strip_model.h @@ -73,10 +73,20 @@ class TabStripModelObserver { bool user_gesture) { } // The specified TabContents at |from_index| was moved to |to_index|. + // TODO(sky): nuke and convert all to 4 arg variant. virtual void TabMoved(TabContents* contents, int from_index, int to_index) { } + // The specified TabContents at |from_index| was moved to |to_index|. If + // the pinned state of the tab is changing |pinned_state_changed| is true. + virtual void TabMoved(TabContents* contents, + int from_index, + int to_index, + bool pinned_state_changed) { + TabMoved(contents, from_index, to_index); + } + // The specified TabContents at |index| changed in some way. |contents| may // be an entirely different object and the old value is no longer available // by the time this message is delivered. @@ -92,6 +102,12 @@ class TabStripModelObserver { virtual void TabChangedAt(TabContents* contents, int index, bool loading_only) { } + // 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 + // notified by way of the TabMoved method with |pinned_state_changed| true. + virtual void TabPinnedStateChanged(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... @@ -202,6 +218,16 @@ 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. +// // A TabStripModel has one delegate that it relies on to perform certain tasks // like creating new TabStripModels (probably hosted in Browser windows) when // required. See TabStripDelegate above for more information. @@ -265,7 +291,8 @@ class TabStripModel : public NotificationObserver { // Adds the specified TabContents in the specified location. If // |inherit_group| is true, the new contents is linked to the current tab's - // group. + // group. If there are pinned tabs at or before |index|, then the newly + // inserted tab is pinned. void InsertTabContentsAt(int index, TabContents* contents, bool foreground, @@ -309,6 +336,7 @@ class TabStripModel : public NotificationObserver { // If |select_after_move| is false, whatever tab was selected before the move // will still be selected, but it's index may have incremented or decremented // one slot. + // See class description for how pinning is effected by this. void MoveTabContentsAt(int index, int to_position, bool select_after_move); // Returns the currently selected TabContents, or NULL if there is none. @@ -384,6 +412,20 @@ class TabStripModel : public NotificationObserver { // should be reset when _any_ selection change occurs in the model. bool ShouldResetGroupOnSelect(TabContents* contents) const; + // Changes the pinned state of the tab at |index|. See description above + // class for details on this. + void SetTabPinned(int index, bool pinned); + + // Returns true if the tab at |index| is pinned. + bool IsTabPinned(int index) const; + + // Returns the index of the first tab that is not pinned. This returns + // |count()| if all of the tabs are pinned, and 0 if no tabs are pinned. + int IndexOfFirstNonPinnedTab() const; + + // Returns true if pinned tabs are enabled in the UI. + static bool IsTabPinningEnabled(); + // Command level API ///////////////////////////////////////////////////////// // Adds a TabContents at the best position in the TabStripModel given the @@ -430,6 +472,7 @@ class TabStripModel : public NotificationObserver { CommandCloseTabsToRight, CommandCloseTabsOpenedBy, CommandRestoreTab, + CommandTogglePinned, CommandLast }; @@ -476,6 +519,10 @@ class TabStripModel : public NotificationObserver { // waiting for the result of an onunload handler. bool InternalCloseTabContentsAt(int index, bool create_historical_tab); + void MoveTabContentsAtImpl(int index, int to_position, + bool select_after_move, + bool update_pinned_state); + TabContents* GetContentsAt(int index) const; // The actual implementation of SelectTabContentsAt. Takes the previously @@ -508,6 +555,26 @@ class TabStripModel : public NotificationObserver { // the TabContents is in the current TabStripModel, unless otherwise // specified in code. struct TabContentsData { + explicit TabContentsData(TabContents* a_contents) + : contents(a_contents), + reset_group_on_select(false), + pinned(false) { + SetGroup(NULL); + } + + // Create a relationship between this TabContents and other TabContentses. + // Used to identify which TabContents to select next after one is closed. + void SetGroup(NavigationController* a_group) { + group = a_group; + opener = a_group; + } + + // Forget the opener relationship so that when this TabContents is closed + // unpredictable re-selection does not occur. + void ForgetOpener() { + opener = NULL; + } + TabContents* contents; // We use NavigationControllers here since they more closely model the // "identity" of a Tab, TabContents can change depending on the URL loaded @@ -533,24 +600,8 @@ class TabStripModel : public NotificationObserver { // relationship is reset to avoid confusing close sequencing. bool reset_group_on_select; - explicit TabContentsData(TabContents* a_contents) - : contents(a_contents), - reset_group_on_select(false) { - SetGroup(NULL); - } - - // Create a relationship between this TabContents and other TabContentses. - // Used to identify which TabContents to select next after one is closed. - void SetGroup(NavigationController* a_group) { - group = a_group; - opener = a_group; - } - - // Forget the opener relationship so that when this TabContents is closed - // unpredictable re-selection does not occur. - void ForgetOpener() { - opener = NULL; - } + // Is the tab pinned? + bool pinned; }; // The TabContents data currently hosted within this TabStripModel. diff --git a/chrome/browser/tabs/tab_strip_model_order_controller.cc b/chrome/browser/tabs/tab_strip_model_order_controller.cc index 1db0525..ad4fb22 100644 --- a/chrome/browser/tabs/tab_strip_model_order_controller.cc +++ b/chrome/browser/tabs/tab_strip_model_order_controller.cc @@ -24,6 +24,7 @@ int TabStripModelOrderController::DetermineInsertionIndex( PageTransition::Type transition, bool foreground) { int tab_count = tabstrip_->count(); + int first_non_pinned_tab = tabstrip_->IndexOfFirstNonPinnedTab(); if (!tab_count) return 0; @@ -33,7 +34,8 @@ int TabStripModelOrderController::DetermineInsertionIndex( // tab, insert it adjacent to the tab that opened that link. // TODO(beng): (http://b/1085481) may want to open right of all locked // tabs? - return tabstrip_->selected_index() + 1; + return std::max(first_non_pinned_tab, + tabstrip_->selected_index() + 1); } NavigationController* opener = &tabstrip_->GetSelectedTabContents()->controller(); @@ -44,7 +46,7 @@ int TabStripModelOrderController::DetermineInsertionIndex( if (index != TabStripModel::kNoTab) return index + 1; // Otherwise insert adjacent to opener... - return tabstrip_->selected_index() + 1; + return std::max(first_non_pinned_tab, tabstrip_->selected_index() + 1); } // In other cases, such as Ctrl+T, open at the end of the strip. return tab_count; diff --git a/chrome/browser/tabs/tab_strip_model_unittest.cc b/chrome/browser/tabs/tab_strip_model_unittest.cc index 19991a3..57795a4 100644 --- a/chrome/browser/tabs/tab_strip_model_unittest.cc +++ b/chrome/browser/tabs/tab_strip_model_unittest.cc @@ -4,6 +4,7 @@ #include "base/file_util.h" #include "base/path_service.h" +#include "base/string_util.h" #include "base/stl_util-inl.h" #include "chrome/browser/dom_ui/new_tab_ui.h" #include "chrome/browser/profile.h" @@ -15,6 +16,7 @@ #include "chrome/browser/tabs/tab_strip_model.h" #include "chrome/browser/tabs/tab_strip_model_order_controller.h" #include "chrome/common/pref_names.h" +#include "chrome/common/property_bag.h" #include "chrome/common/url_constants.h" #include "testing/gtest/include/gtest/gtest.h" @@ -98,9 +100,43 @@ class TabStripModelTest : public RenderViewHostTestHarness { //contents()->DidBecomeSelected(); } + // Sets the id of the specified contents. + void SetID(TabContents* contents, int id) { + GetIDAccessor()->SetProperty(contents->property_bag(), id); + } + + // Returns the id of the specified contents. + int GetID(TabContents* contents) { + return *GetIDAccessor()->GetProperty(contents->property_bag()); + } + + // Returns the state of the given tab strip as a string. The state consists + // of the ID of each tab contents followed by a 'p' if pinned. For example, + // if the model consists of two tabs with ids 2 and 1, with the first + // tab pinned, this returns "2p 1". + std::string GetPinnedState(const TabStripModel& model) { + std::string actual; + for (int i = 0; i < model.count(); ++i) { + if (i > 0) + actual += " "; + + actual += IntToString(GetID(model.GetTabContentsAt(i))); + + if (model.IsTabPinned(i)) + actual += "p"; + } + return actual; + } + private: + PropertyAccessor<int>* GetIDAccessor() { + static PropertyAccessor<int> accessor; + return &accessor; + } + std::wstring test_dir_; std::wstring profile_path_; + std::map<TabContents*,int> foo_; ProfileManager pm_; }; @@ -117,7 +153,8 @@ class MockTabStripModelObserver : public TabStripModelObserver { DETACH, SELECT, MOVE, - CHANGE + CHANGE, + PINNED }; struct State { @@ -130,6 +167,7 @@ class MockTabStripModelObserver : public TabStripModelObserver { dst_index(a_dst_index), user_gesture(false), foreground(false), + pinned_state_changed(false), action(a_action) { } @@ -139,6 +177,7 @@ class MockTabStripModelObserver : public TabStripModelObserver { int dst_index; bool user_gesture; bool foreground; + bool pinned_state_changed; TabStripModelObserverAction action; }; @@ -159,6 +198,7 @@ class MockTabStripModelObserver : public TabStripModelObserver { EXPECT_EQ(s->dst_index, state.dst_index); EXPECT_EQ(s->user_gesture, state.user_gesture); EXPECT_EQ(s->foreground, state.foreground); + EXPECT_EQ(s->pinned_state_changed, state.pinned_state_changed); EXPECT_EQ(s->action, state.action); return (s->src_contents == state.src_contents && s->dst_contents == state.dst_contents && @@ -166,6 +206,7 @@ class MockTabStripModelObserver : public TabStripModelObserver { s->dst_index == state.dst_index && s->user_gesture == state.user_gesture && s->foreground == state.foreground && + s->pinned_state_changed == state.pinned_state_changed && s->action == state.action); } @@ -188,9 +229,11 @@ class MockTabStripModelObserver : public TabStripModelObserver { states_.push_back(s); } virtual void TabMoved( - TabContents* contents, int from_index, int to_index) { + TabContents* contents, int from_index, int to_index, + bool pinned_state_changed) { State* s = new State(contents, to_index, MOVE); s->src_index = from_index; + s->pinned_state_changed = pinned_state_changed; states_.push_back(s); } @@ -204,6 +247,9 @@ class MockTabStripModelObserver : public TabStripModelObserver { bool loading_only) { states_.push_back(new State(contents, index, CHANGE)); } + virtual void TabPinnedStateChanged(TabContents* contents, int index) { + states_.push_back(new State(contents, index, PINNED)); + } virtual void TabStripEmpty() { empty_ = true; } @@ -1210,3 +1256,209 @@ TEST_F(TabStripModelTest, NavigationForgettingDoesntAffectNewTab) { strip.CloseAllTabs(); } + +// Tests various permutations of pinning tabs. +TEST_F(TabStripModelTest, Pinning) { + TabStripDummyDelegate delegate(NULL); + TabStripModel tabstrip(&delegate, profile()); + MockTabStripModelObserver observer; + tabstrip.AddObserver(&observer); + + EXPECT_TRUE(tabstrip.empty()); + + typedef MockTabStripModelObserver::State State; + + TabContents* contents1 = CreateTabContents(); + TabContents* contents2 = CreateTabContents(); + TabContents* contents3 = CreateTabContents(); + + SetID(contents1, 1); + SetID(contents2, 2); + SetID(contents3, 3); + + // Note! The ordering of these tests is important, each subsequent test + // builds on the state established in the previous. This is important if you + // ever insert tests rather than append. + + // Initial state, three tabs, first selected. + tabstrip.AppendTabContents(contents1, true); + tabstrip.AppendTabContents(contents2, false); + tabstrip.AppendTabContents(contents3, false); + + observer.ClearStates(); + + // Pin the first tab, this shouldn't visually reorder anything. + { + tabstrip.SetTabPinned(0, true); + + // As the order didn't change, we should get a pinned notification. + ASSERT_EQ(1, observer.GetStateCount()); + State state(contents1, 0, MockTabStripModelObserver::PINNED); + EXPECT_TRUE(observer.StateEquals(0, state)); + + // And verify the state. + EXPECT_EQ("1p 2 3", GetPinnedState(tabstrip)); + + observer.ClearStates(); + } + + // Unpin the first tab. + { + tabstrip.SetTabPinned(0, false); + + // As the order didn't change, we should get a pinned notification. + ASSERT_EQ(1, observer.GetStateCount()); + State state(contents1, 0, MockTabStripModelObserver::PINNED); + EXPECT_TRUE(observer.StateEquals(0, state)); + + // And verify the state. + EXPECT_EQ("1 2 3", GetPinnedState(tabstrip)); + + observer.ClearStates(); + } + + // Pin the 3rd tab, which should move it to the front. + { + tabstrip.SetTabPinned(2, true); + + // The pinning should have resulted in a move. + ASSERT_EQ(1, observer.GetStateCount()); + State state(contents3, 0, MockTabStripModelObserver::MOVE); + state.src_index = 2; + state.pinned_state_changed = true; + EXPECT_TRUE(observer.StateEquals(0, state)); + + // And verify the state. + EXPECT_EQ("3p 1 2", GetPinnedState(tabstrip)); + + observer.ClearStates(); + } + + // Pin the tab "1", which shouldn't move anything. + { + tabstrip.SetTabPinned(1, true); + + // As the order didn't change, we should get a pinned notification. + ASSERT_EQ(1, observer.GetStateCount()); + State state(contents1, 1, MockTabStripModelObserver::PINNED); + EXPECT_TRUE(observer.StateEquals(0, state)); + + // And verify the state. + EXPECT_EQ("3p 1p 2", GetPinnedState(tabstrip)); + + observer.ClearStates(); + } + + // Move tab "2" to the front, which should pin it. + { + tabstrip.MoveTabContentsAt(2, 0, false); + + // As the order didn't change, we should get a pinned notification. + ASSERT_EQ(1, observer.GetStateCount()); + State state(contents2, 0, MockTabStripModelObserver::MOVE); + state.src_index = 2; + state.pinned_state_changed = true; + EXPECT_TRUE(observer.StateEquals(0, state)); + + // And verify the state. + EXPECT_EQ("2p 3p 1p", GetPinnedState(tabstrip)); + + observer.ClearStates(); + } + + // Unpin tab "2", which implicitly moves it to the end. + { + tabstrip.SetTabPinned(0, false); + + ASSERT_EQ(1, observer.GetStateCount()); + State state(contents2, 2, MockTabStripModelObserver::MOVE); + state.src_index = 0; + state.pinned_state_changed = true; + EXPECT_TRUE(observer.StateEquals(0, state)); + + // And verify the state. + EXPECT_EQ("3p 1p 2", GetPinnedState(tabstrip)); + + observer.ClearStates(); + } + + // Drag tab "3" to after "1', which should not change the pinned state. + { + tabstrip.MoveTabContentsAt(0, 1, false); + + ASSERT_EQ(1, observer.GetStateCount()); + State state(contents3, 1, MockTabStripModelObserver::MOVE); + state.src_index = 0; + EXPECT_TRUE(observer.StateEquals(0, state)); + + // And verify the state. + EXPECT_EQ("1p 3p 2", GetPinnedState(tabstrip)); + + observer.ClearStates(); + } + + // Unpin tab "1". + { + tabstrip.SetTabPinned(0, false); + + ASSERT_EQ(1, observer.GetStateCount()); + State state(contents1, 1, MockTabStripModelObserver::MOVE); + state.src_index = 0; + state.pinned_state_changed = true; + EXPECT_TRUE(observer.StateEquals(0, state)); + + // And verify the state. + EXPECT_EQ("3p 1 2", GetPinnedState(tabstrip)); + + observer.ClearStates(); + } + + // Unpin tab "3". + { + tabstrip.SetTabPinned(0, false); + + ASSERT_EQ(1, observer.GetStateCount()); + State state(contents3, 0, MockTabStripModelObserver::PINNED); + EXPECT_TRUE(observer.StateEquals(0, state)); + + EXPECT_EQ("3 1 2", GetPinnedState(tabstrip)); + + observer.ClearStates(); + } + + // Unpin tab "3" again, as it's unpinned nothing should change. + { + tabstrip.SetTabPinned(0, false); + + ASSERT_EQ(0, observer.GetStateCount()); + + EXPECT_EQ("3 1 2", GetPinnedState(tabstrip)); + } + + // Pin "3" and "1". + { + tabstrip.SetTabPinned(0, true); + tabstrip.SetTabPinned(1, true); + + EXPECT_EQ("3p 1p 2", GetPinnedState(tabstrip)); + + observer.ClearStates(); + } + + TabContents* contents4 = CreateTabContents(); + SetID(contents4, 4); + + // Insert "4" between "3" and "1". As "3" and "1" are pinned, "4" should + // be pinned too. + { + tabstrip.InsertTabContentsAt(1, contents4, false, false); + + ASSERT_EQ(1, observer.GetStateCount()); + State state(contents4, 1, MockTabStripModelObserver::INSERT); + EXPECT_TRUE(observer.StateEquals(0, state)); + + EXPECT_EQ("3p 4p 1p 2", GetPinnedState(tabstrip)); + } + + tabstrip.CloseAllTabs(); +} |