summaryrefslogtreecommitdiffstats
path: root/chrome/browser/tabs
diff options
context:
space:
mode:
Diffstat (limited to 'chrome/browser/tabs')
-rw-r--r--chrome/browser/tabs/tab_strip_model.cc120
-rw-r--r--chrome/browser/tabs/tab_strip_model.h89
-rw-r--r--chrome/browser/tabs/tab_strip_model_order_controller.cc6
-rw-r--r--chrome/browser/tabs/tab_strip_model_unittest.cc256
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();
+}