summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsky@chromium.org <sky@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2011-03-09 19:47:01 +0000
committersky@chromium.org <sky@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2011-03-09 19:47:01 +0000
commitfe348bee89e025349af55425e6f7aa554b668891 (patch)
treeaf28cd5f32a8c04b386d59b63f6ad3ed5ac975db
parent9d1e2ba0d663c5f393410f71f3310b8e6a36747f (diff)
downloadchromium_src-fe348bee89e025349af55425e6f7aa554b668891.zip
chromium_src-fe348bee89e025349af55425e6f7aa554b668891.tar.gz
chromium_src-fe348bee89e025349af55425e6f7aa554b668891.tar.bz2
Creates TabStripSelectionModel and moves some functionality to
it. Also adds MoveSelectedTabsTo which will be used when dragging around in the tab strip. BUG=30572 TEST=none, just make sure tab selection isn't broke. Review URL: http://codereview.chromium.org/6647011 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@77506 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r--chrome/browser/tabs/tab_strip_model.cc216
-rw-r--r--chrome/browser/tabs/tab_strip_model.h52
-rw-r--r--chrome/browser/tabs/tab_strip_model_unittest.cc86
-rw-r--r--chrome/browser/tabs/tab_strip_selection_model.cc129
-rw-r--r--chrome/browser/tabs/tab_strip_selection_model.h104
-rw-r--r--chrome/browser/tabs/tab_strip_selection_model_unittest.cc141
-rw-r--r--chrome/chrome_browser.gypi2
-rw-r--r--chrome/chrome_tests.gypi1
8 files changed, 673 insertions, 58 deletions
diff --git a/chrome/browser/tabs/tab_strip_model.cc b/chrome/browser/tabs/tab_strip_model.cc
index f12af14..c855e10 100644
--- a/chrome/browser/tabs/tab_strip_model.cc
+++ b/chrome/browser/tabs/tab_strip_model.cc
@@ -60,7 +60,6 @@ bool TabStripModelDelegate::CanCloseTab() const {
TabStripModel::TabStripModel(TabStripModelDelegate* delegate, Profile* profile)
: delegate_(delegate),
- selected_index_(kNoTab),
profile_(profile),
closing_all_(false),
order_controller_(NULL) {
@@ -152,17 +151,15 @@ void TabStripModel::InsertTabContentsAt(int index,
contents_data_.insert(contents_data_.begin() + index, data);
- if (index <= selected_index_) {
- // If a tab is inserted before the current selected index,
- // then |selected_index| needs to be incremented.
- ++selected_index_;
- }
+ selection_model_.IncrementFrom(index);
FOR_EACH_OBSERVER(TabStripModelObserver, observers_,
- TabInsertedAt(contents, index, foreground));
+ TabInsertedAt(contents, index, foreground));
- if (foreground)
- ChangeSelectedContentsFrom(selected_contents, index, false);
+ if (foreground) {
+ selection_model_.SetSelectedIndex(index);
+ NotifyTabSelectedIfChanged(selected_contents, index, false);
+ }
}
TabContentsWrapper* TabStripModel::ReplaceTabContentsAt(
@@ -181,10 +178,10 @@ TabContentsWrapper* TabStripModel::ReplaceTabContentsAt(
// When the selected tab contents is replaced send out selected notification
// too. We do this as nearly all observers need to treat a replace of the
// selected contents as selection changing.
- if (selected_index_ == index) {
+ if (selected_index() == index) {
FOR_EACH_OBSERVER(TabStripModelObserver, observers_,
TabSelectedAt(old_contents, new_contents,
- selected_index_, false));
+ selected_index(), false));
}
return old_contents;
}
@@ -222,12 +219,22 @@ TabContentsWrapper* TabStripModel::DetachTabContentsAt(int index) {
// a second pass.
FOR_EACH_OBSERVER(TabStripModelObserver, observers_, TabStripEmpty());
} else {
- if (index == selected_index_) {
- ChangeSelectedContentsFrom(removed_contents, next_selected_index, false);
- } else if (index < selected_index_) {
- // The selected tab didn't change, but its position shifted; update our
- // index to continue to point at it.
- --selected_index_;
+ int old_selected = selected_index();
+ selection_model_.DecrementFrom(index);
+ if (index == old_selected) {
+ if (!selection_model_.empty()) {
+ // A selected tab was removed, but there is still something selected.
+ // Move the active and anchor to the first selected index.
+ selection_model_.set_active(selection_model_.selected_indices()[0]);
+ selection_model_.set_anchor(selection_model_.active());
+ NotifyTabSelectedIfChanged(removed_contents, selected_index(), false);
+ } else {
+ // The selected tab was removed and nothing is selected. Reset the
+ // selection and send out notification.
+ selection_model_.SetSelectedIndex(next_selected_index);
+ NotifyTabSelectedIfChanged(removed_contents, next_selected_index,
+ false);
+ }
}
}
return removed_contents;
@@ -235,10 +242,15 @@ TabContentsWrapper* TabStripModel::DetachTabContentsAt(int index) {
void TabStripModel::SelectTabContentsAt(int index, bool user_gesture) {
DCHECK(ContainsIndex(index));
- ChangeSelectedContentsFrom(GetSelectedTabContents(), index, user_gesture);
+ TabContentsWrapper* old =
+ (selected_index() == TabStripSelectionModel::kUnselectedIndex) ?
+ NULL : GetSelectedTabContents();
+ selection_model_.SetSelectedIndex(index);
+ NotifyTabSelectedIfChanged(old, index, user_gesture);
}
-void TabStripModel::MoveTabContentsAt(int index, int to_position,
+void TabStripModel::MoveTabContentsAt(int index,
+ int to_position,
bool select_after_move) {
DCHECK(ContainsIndex(index));
if (index == to_position)
@@ -255,8 +267,36 @@ void TabStripModel::MoveTabContentsAt(int index, int to_position,
MoveTabContentsAtImpl(index, to_position, select_after_move);
}
+void TabStripModel::MoveSelectedTabsTo(int index) {
+ size_t selected_pinned_count = 0;
+ size_t selected_count = selection_model_.selected_indices().size();
+ for (size_t i = 0; i < selected_count &&
+ IsTabPinned(selection_model_.selected_indices()[i]); ++i) {
+ selected_pinned_count++;
+ }
+
+ size_t total_pinned_count = 0;
+ for (int i = 0; i < count() && IsTabPinned(i); ++i)
+ total_pinned_count++;
+
+ // To maintain that all pinned tabs occur before non-pinned tabs we move them
+ // first.
+ if (selected_pinned_count > 0) {
+ MoveSelectedTabsToImpl(
+ std::min(static_cast<int>(total_pinned_count - selected_pinned_count),
+ index), 0u, selected_pinned_count);
+ }
+ if (selected_pinned_count == selected_count)
+ return;
+
+ // Then move the non-pinned tabs.
+ MoveSelectedTabsToImpl(std::max(index, static_cast<int>(total_pinned_count)),
+ selected_pinned_count,
+ selected_count - selected_pinned_count);
+}
+
TabContentsWrapper* TabStripModel::GetSelectedTabContents() const {
- return GetTabContentsAt(selected_index_);
+ return GetTabContentsAt(selected_index());
}
TabContentsWrapper* TabStripModel::GetTabContentsAt(int index) const {
@@ -511,6 +551,49 @@ int TabStripModel::ConstrainInsertionIndex(int index, bool mini_tab) {
std::min(count(), std::max(index, IndexOfFirstNonMiniTab()));
}
+void TabStripModel::ExtendSelectionTo(int index) {
+ DCHECK(ContainsIndex(index));
+ int old_selection = selected_index();
+ selection_model_.SetSelectionFromAnchorTo(index);
+ // This may not have resulted in a change, but we assume it did.
+ NotifySelectionChanged(old_selection);
+}
+
+void TabStripModel::ToggleSelectionAt(int index) {
+ DCHECK(ContainsIndex(index));
+ int old_selection = selected_index();
+ if (selection_model_.IsSelected(index)) {
+ if (selection_model_.size() == 1) {
+ // One tab must be selected and this tab is currently selected so we can't
+ // unselect it.
+ return;
+ }
+ selection_model_.RemoveIndexFromSelection(index);
+ selection_model_.set_anchor(index);
+ if (selection_model_.active() == TabStripSelectionModel::kUnselectedIndex)
+ selection_model_.set_active(selection_model_.selected_indices()[0]);
+ } else {
+ selection_model_.AddIndexToSelection(index);
+ selection_model_.set_anchor(index);
+ selection_model_.set_active(index);
+ }
+ NotifySelectionChanged(old_selection);
+}
+
+bool TabStripModel::IsTabSelected(int index) {
+ DCHECK(ContainsIndex(index));
+ return selection_model_.IsSelected(index);
+}
+
+void TabStripModel::SetSelectionFromModel(
+ const TabStripSelectionModel& source) {
+ DCHECK_NE(TabStripSelectionModel::kUnselectedIndex, source.active());
+ int old_selected_index = selected_index();
+ selection_model_.Copy(source);
+ // This may not have resulted in a change, but we assume it did.
+ NotifySelectionChanged(old_selected_index);
+}
+
void TabStripModel::AddTabContents(TabContentsWrapper* contents,
int index,
PageTransition::Type transition,
@@ -579,7 +662,8 @@ void TabStripModel::AddTabContents(TabContentsWrapper* contents,
}
void TabStripModel::CloseSelectedTab() {
- CloseTabContentsAt(selected_index_, CLOSE_CREATE_HISTORICAL_TAB);
+ // TODO: this should close all selected tabs.
+ CloseTabContentsAt(selected_index(), CLOSE_CREATE_HISTORICAL_TAB);
}
void TabStripModel::SelectNextTab() {
@@ -595,13 +679,15 @@ void TabStripModel::SelectLastTab() {
}
void TabStripModel::MoveTabNext() {
- int new_index = std::min(selected_index_ + 1, count() - 1);
- MoveTabContentsAt(selected_index_, new_index, true);
+ // TODO: this likely needs to be updated for multi-selection.
+ int new_index = std::min(selected_index() + 1, count() - 1);
+ MoveTabContentsAt(selected_index(), new_index, true);
}
void TabStripModel::MoveTabPrevious() {
- int new_index = std::max(selected_index_ - 1, 0);
- MoveTabContentsAt(selected_index_, new_index, true);
+ // TODO: this likely needs to be updated for multi-selection.
+ int new_index = std::max(selected_index() - 1, 0);
+ MoveTabContentsAt(selected_index(), new_index, true);
}
// Context menu functions.
@@ -950,8 +1036,9 @@ TabContentsWrapper* TabStripModel::GetContentsAt(int index) const {
return contents_data_.at(index)->contents;
}
-void TabStripModel::ChangeSelectedContentsFrom(
- TabContentsWrapper* old_contents, int to_index, bool user_gesture) {
+void TabStripModel::NotifyTabSelectedIfChanged(TabContentsWrapper* old_contents,
+ int to_index,
+ bool user_gesture) {
TabContentsWrapper* new_contents = GetContentsAt(to_index);
if (old_contents == new_contents)
return;
@@ -962,17 +1049,20 @@ void TabStripModel::ChangeSelectedContentsFrom(
TabDeselected(last_selected_contents));
}
- selected_index_ = to_index;
- ObserverListBase<TabStripModelObserver>::Iterator it(observers_);
- TabStripModelObserver* obs;
- while ((obs = it.GetNext()) != NULL)
- obs->TabSelectedAt(last_selected_contents, new_contents,
- selected_index_, user_gesture);
- /*
FOR_EACH_OBSERVER(TabStripModelObserver, observers_,
- TabSelectedAt(last_selected_contents, new_contents, selected_index_,
- user_gesture));
- */
+ TabSelectedAt(last_selected_contents, new_contents,
+ selected_index(), user_gesture));
+}
+
+void TabStripModel::NotifySelectionChanged(int old_selected_index) {
+ TabContentsWrapper* old_tab =
+ old_selected_index == TabStripSelectionModel::kUnselectedIndex ?
+ NULL : GetTabContentsAt(old_selected_index);
+ TabContentsWrapper* new_tab =
+ selected_index() == TabStripSelectionModel::kUnselectedIndex ?
+ NULL : GetTabContentsAt(selected_index());
+ FOR_EACH_OBSERVER(TabStripModelObserver, observers_,
+ TabSelectedAt(old_tab, new_tab, selected_index(), true));
}
void TabStripModel::SelectRelativeTab(bool next) {
@@ -981,31 +1071,65 @@ void TabStripModel::SelectRelativeTab(bool next) {
if (contents_data_.empty())
return;
- int index = selected_index_;
+ int index = selected_index();
int delta = next ? 1 : -1;
index = (index + count() + delta) % count();
SelectTabContentsAt(index, true);
}
-void TabStripModel::MoveTabContentsAtImpl(int index, int to_position,
+void TabStripModel::MoveTabContentsAtImpl(int index,
+ int to_position,
bool select_after_move) {
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_++;
+ selection_model_.Move(index, to_position);
+ if (!selection_model_.IsSelected(select_after_move) && select_after_move) {
+ // TODO(sky): why doesn't this code notify observers?
+ selection_model_.SetSelectedIndex(to_position);
}
FOR_EACH_OBSERVER(TabStripModelObserver, observers_,
TabMoved(moved_data->contents, index, to_position));
}
+void TabStripModel::MoveSelectedTabsToImpl(int index,
+ size_t start,
+ size_t length) {
+ DCHECK(start < selection_model_.selected_indices().size() &&
+ start + length <= selection_model_.selected_indices().size());
+ size_t end = start + length;
+ int count_before_index = 0;
+ for (size_t i = start; i < end &&
+ selection_model_.selected_indices()[i] < index + count_before_index;
+ ++i) {
+ count_before_index++;
+ }
+
+ // First move those before index. Any tabs before index end up moving in the
+ // selection model so we use start each time through.
+ int target_index = index + count_before_index;
+ size_t tab_index = start;
+ while (tab_index < end &&
+ selection_model_.selected_indices()[start] < index) {
+ MoveTabContentsAt(selection_model_.selected_indices()[start],
+ target_index - 1, false);
+ tab_index++;
+ }
+
+ // Then move those after the index. These don't result in reordering the
+ // selection.
+ while (tab_index < end) {
+ if (selection_model_.selected_indices()[tab_index] != target_index) {
+ MoveTabContentsAt(selection_model_.selected_indices()[tab_index],
+ target_index, false);
+ }
+ tab_index++;
+ target_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 67e3ec4..a9bb9c0 100644
--- a/chrome/browser/tabs/tab_strip_model.h
+++ b/chrome/browser/tabs/tab_strip_model.h
@@ -10,9 +10,10 @@
#include "base/observer_list.h"
#include "chrome/browser/tabs/tab_strip_model_observer.h"
+#include "chrome/browser/tabs/tab_strip_selection_model.h"
+#include "chrome/common/notification_observer.h"
+#include "chrome/common/notification_registrar.h"
#include "chrome/common/page_transition_types.h"
-#include "content/common/notification_observer.h"
-#include "content/common/notification_registrar.h"
class NavigationController;
class Profile;
@@ -132,7 +133,9 @@ class TabStripModel : public NotificationObserver {
Profile* profile() const { return profile_; }
// Retrieve the index of the currently selected TabContents.
- int selected_index() const { return selected_index_; }
+ // TODO(sky): rename this to active and update similar places (observer,
+ // other methods...).
+ int selected_index() const { return selection_model_.active(); }
// Returns true if the tabstrip is currently closing all open tabs (via a
// call to CloseAllTabs). As tabs close, the selection in the tabstrip
@@ -223,6 +226,12 @@ class TabStripModel : public NotificationObserver {
// tabs mixing.
void MoveTabContentsAt(int index, int to_position, bool select_after_move);
+ // Moves the selected tabs to |index|. |index| is treated as if the tab strip
+ // did not contain any of the selected tabs. For example, if the tabstrip
+ // contains [A b c D E f] (upper case selected) and this is invoked with 1 the
+ // result is [b A D E c f].
+ void MoveSelectedTabsTo(int index);
+
// Returns the currently selected TabContents, or NULL if there is none.
TabContentsWrapper* GetSelectedTabContents() const;
@@ -346,6 +355,19 @@ class TabStripModel : public NotificationObserver {
// is between IndexOfFirstNonMiniTab and count().
int ConstrainInsertionIndex(int index, bool mini_tab);
+ // Extends the selection from the anchor to |index|.
+ void ExtendSelectionTo(int index);
+
+ // Toggles the selection at |index|. This does nothing if |index| is selected
+ // and there are no other selected tabs.
+ void ToggleSelectionAt(int index);
+
+ // Returns true if the tab at |index| is selected.
+ bool IsTabSelected(int index);
+
+ // Sets the selection to match that of |source|.
+ void SetSelectionFromModel(const TabStripSelectionModel& source);
+
// Command level API /////////////////////////////////////////////////////////
// Adds a TabContents at the best position in the TabStripModel given the
@@ -452,12 +474,15 @@ class TabStripModel : public NotificationObserver {
TabContentsWrapper* GetContentsAt(int index) const;
- // The actual implementation of SelectTabContentsAt. Takes the previously
- // selected contents in |old_contents|, which may actually not be in
- // |contents_| anymore because it may have been removed by a call to say
- // DetachTabContentsAt...
- void ChangeSelectedContentsFrom(
- TabContentsWrapper* old_contents, int to_index, bool user_gesture);
+ // If the TabContentsWrapper at |to_index| differs from |old_contents|
+ // notifies observers.
+ void NotifyTabSelectedIfChanged(TabContentsWrapper* old_contents,
+ int to_index,
+ bool user_gesture);
+
+ // Notifies the observers the selection changed. |old_selected_index| gives
+ // the old selected index.
+ void NotifySelectionChanged(int old_selected_index);
// Returns the number of New Tab tabs in the TabStripModel.
int GetNewTabCount() const;
@@ -472,6 +497,10 @@ class TabStripModel : public NotificationObserver {
int to_position,
bool select_after_move);
+ // Implementation of MoveSelectedTabsTo. Moves |length| of the selected tabs
+ // starting at |start| to |index|. See MoveSelectedTabsTo for more details.
+ void MoveSelectedTabsToImpl(int index, size_t start, size_t length);
+
// 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.
@@ -548,9 +577,6 @@ class TabStripModel : public NotificationObserver {
typedef std::vector<TabContentsData*> TabContentsDataVector;
TabContentsDataVector contents_data_;
- // The index of the TabContents in |contents_| that is currently selected.
- int selected_index_;
-
// A profile associated with this TabStripModel, used when creating new Tabs.
Profile* profile_;
@@ -568,6 +594,8 @@ class TabStripModel : public NotificationObserver {
// A scoped container for notification registries.
NotificationRegistrar registrar_;
+ TabStripSelectionModel selection_model_;
+
DISALLOW_COPY_AND_ASSIGN(TabStripModel);
};
diff --git a/chrome/browser/tabs/tab_strip_model_unittest.cc b/chrome/browser/tabs/tab_strip_model_unittest.cc
index c8d5222..d6442e2 100644
--- a/chrome/browser/tabs/tab_strip_model_unittest.cc
+++ b/chrome/browser/tabs/tab_strip_model_unittest.cc
@@ -11,6 +11,7 @@
#include "base/scoped_ptr.h"
#include "base/stl_util-inl.h"
#include "base/string_number_conversions.h"
+#include "base/string_split.h"
#include "base/string_util.h"
#include "base/utf_string_conversions.h"
#include "chrome/browser/defaults.h"
@@ -220,6 +221,30 @@ class TabStripModelTest : public RenderViewHostTestHarness {
return result;
}
+ void PrepareTabstripForSelectionTest(TabStripModel* model,
+ int tab_count,
+ int pinned_count,
+ const std::string& selected_tabs) {
+ for (int i = 0; i < tab_count; ++i) {
+ TabContentsWrapper* contents = CreateTabContents();
+ SetID(contents->tab_contents(), i);
+ model->AppendTabContents(contents, true);
+ }
+ for (int i = 0; i < pinned_count; ++i)
+ model->SetTabPinned(i, true);
+
+ TabStripSelectionModel selection_model;
+ std::vector<std::string> selection;
+ base::SplitStringAlongWhitespace(selected_tabs, &selection);
+ for (size_t i = 0; i < selection.size(); ++i) {
+ int value;
+ ASSERT_TRUE(base::StringToInt(selection[i], &value));
+ selection_model.AddIndexToSelection(value);
+ }
+ selection_model.set_active(selection_model.selected_indices()[0]);
+ model->SetSelectionFromModel(selection_model);
+ }
+
private:
PropertyAccessor<int>* GetIDAccessor() {
static PropertyAccessor<int> accessor;
@@ -1898,3 +1923,64 @@ TEST_F(TabStripModelTest, DeleteFromDestroy) {
DeleteTabContentsOnDestroyedObserver observer(contents2, contents1);
strip.CloseAllTabs();
}
+
+TEST_F(TabStripModelTest, MoveSelectedTabsTo) {
+ struct TestData {
+ // Number of tabs the tab strip should have.
+ const int tab_count;
+
+ // Number of pinned tabs.
+ const int pinned_count;
+
+ // Index of the tabs to select.
+ const std::string selected_tabs;
+
+ // Index to move the tabs to.
+ const int target_index;
+
+ // Expected state after the move (space separated list of indices).
+ const std::string state_after_move;
+ } test_data[] = {
+ // 1 selected tab.
+ { 2, 0, "0", 1, "1 0" },
+ { 3, 0, "0", 2, "1 2 0" },
+ { 3, 0, "2", 0, "2 0 1" },
+ { 3, 0, "2", 1, "0 2 1" },
+ { 3, 0, "0 1", 0, "0 1 2" },
+
+ // 2 selected tabs.
+ { 6, 0, "4 5", 1, "0 4 5 1 2 3" },
+ { 3, 0, "0 1", 1, "2 0 1" },
+ { 4, 0, "0 2", 1, "1 0 2 3" },
+ { 6, 0, "0 1", 3, "2 3 4 0 1 5" },
+
+ // 3 selected tabs.
+ { 6, 0, "0 2 3", 3, "1 4 5 0 2 3" },
+ { 7, 0, "4 5 6", 1, "0 4 5 6 1 2 3" },
+ { 7, 0, "1 5 6", 4, "0 2 3 4 1 5 6" },
+
+ // 5 selected tabs.
+ { 8, 0, "0 2 3 6 7", 3, "1 4 5 0 2 3 6 7" },
+
+ // 7 selected tabs
+ { 16, 0, "0 1 2 3 4 7 9", 8, "5 6 8 10 11 12 13 14 0 1 2 3 4 7 9 15" },
+
+ // With pinned tabs.
+ { 6, 2, "2 3", 2, "0p 1p 2 3 4 5" },
+ { 6, 2, "0 4", 3, "1p 0p 2 4 3 5" },
+ { 6, 3, "1 2 4", 0, "1p 2p 0p 4 3 5" },
+ { 8, 3, "1 3 4", 4, "0p 2p 1p 5 3 4 6 7" },
+ };
+
+ for (size_t i = 0; i < ARRAYSIZE_UNSAFE(test_data); ++i) {
+ TabStripDummyDelegate delegate(NULL);
+ TabStripModel strip(&delegate, profile());
+ ASSERT_NO_FATAL_FAILURE(
+ PrepareTabstripForSelectionTest(&strip, test_data[i].tab_count,
+ test_data[i].pinned_count,
+ test_data[i].selected_tabs));
+ strip.MoveSelectedTabsTo(test_data[i].target_index);
+ EXPECT_EQ(test_data[i].state_after_move, GetPinnedState(strip)) << i;
+ strip.CloseAllTabs();
+ }
+}
diff --git a/chrome/browser/tabs/tab_strip_selection_model.cc b/chrome/browser/tabs/tab_strip_selection_model.cc
new file mode 100644
index 0000000..b02152f
--- /dev/null
+++ b/chrome/browser/tabs/tab_strip_selection_model.cc
@@ -0,0 +1,129 @@
+// Copyright (c) 2010 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/tabs/tab_strip_selection_model.h"
+
+#include <algorithm>
+#include <valarray>
+
+#include "base/logging.h"
+
+// static
+const int TabStripSelectionModel::kUnselectedIndex = -1;
+
+static void IncrementFromImpl(int index, int* value) {
+ if (*value >= index)
+ (*value)++;
+}
+
+static bool DecrementFromImpl(int index, int* value) {
+ if (*value == index) {
+ *value = TabStripSelectionModel::kUnselectedIndex;
+ return true;
+ }
+ if (*value > index)
+ (*value)--;
+ return false;
+}
+
+TabStripSelectionModel::TabStripSelectionModel()
+ : active_(kUnselectedIndex),
+ anchor_(kUnselectedIndex) {
+}
+
+TabStripSelectionModel::~TabStripSelectionModel() {
+}
+
+void TabStripSelectionModel::IncrementFrom(int index) {
+ // Shift the selection to account for the newly inserted tab.
+ for (SelectedIndices::iterator i = selected_indices_.begin();
+ i != selected_indices_.end(); ++i) {
+ IncrementFromImpl(index, &(*i));
+ }
+ IncrementFromImpl(index, &anchor_);
+ IncrementFromImpl(index, &active_);
+}
+
+void TabStripSelectionModel::DecrementFrom(int index) {
+ for (SelectedIndices::iterator i = selected_indices_.begin();
+ i != selected_indices_.end(); ) {
+ if (DecrementFromImpl(index, &(*i)))
+ i = selected_indices_.erase(i);
+ else
+ ++i;
+ }
+ DecrementFromImpl(index, &anchor_);
+ DecrementFromImpl(index, &active_);
+}
+
+void TabStripSelectionModel::SetSelectedIndex(int index) {
+ anchor_ = active_ = index;
+ SetSelectionFromAnchorTo(index);
+}
+
+bool TabStripSelectionModel::IsSelected(int index) {
+ return std::find(selected_indices_.begin(), selected_indices_.end(), index) !=
+ selected_indices_.end();
+}
+
+void TabStripSelectionModel::AddIndexToSelection(int index) {
+ if (!IsSelected(index)) {
+ selected_indices_.push_back(index);
+ std::sort(selected_indices_.begin(), selected_indices_.end());
+ }
+}
+
+void TabStripSelectionModel::RemoveIndexFromSelection(int index) {
+ SelectedIndices::iterator i = std::find(selected_indices_.begin(),
+ selected_indices_.end(), index);
+ if (i != selected_indices_.end())
+ selected_indices_.erase(i);
+}
+
+void TabStripSelectionModel::SetSelectionFromAnchorTo(int index) {
+ if (anchor_ == kUnselectedIndex) {
+ anchor_ = index;
+ active_ = index;
+ std::sort(selected_indices_.begin(), selected_indices_.end());
+ } else {
+ int delta = std::abs(index - anchor_);
+ SelectedIndices new_selection(delta + 1, 0);
+ for (int i = 0, min = std::min(index, anchor_); i <= delta; ++i)
+ new_selection[i] = i + min;
+ selected_indices_.swap(new_selection);
+ active_ = index;
+ }
+}
+
+void TabStripSelectionModel::Move(int from, int to) {
+ DCHECK_NE(to, from);
+ bool was_anchor = from == anchor_;
+ bool was_active = from == active_;
+ bool was_selected = IsSelected(from);
+ if (to < from) {
+ IncrementFrom(to);
+ DecrementFrom(from + 1);
+ } else {
+ DecrementFrom(from);
+ IncrementFrom(to);
+ }
+ if (was_active)
+ active_ = to;
+ if (was_anchor)
+ anchor_ = to;
+ if (was_selected)
+ AddIndexToSelection(to);
+}
+
+void TabStripSelectionModel::Clear() {
+ anchor_ = active_ = kUnselectedIndex;
+ SelectedIndices empty_selection;
+ selected_indices_.swap(empty_selection);
+}
+
+void TabStripSelectionModel::Copy(const TabStripSelectionModel& source) {
+ selected_indices_ = source.selected_indices_;
+ active_ = source.active_;
+ anchor_ = source.anchor_;
+}
diff --git a/chrome/browser/tabs/tab_strip_selection_model.h b/chrome/browser/tabs/tab_strip_selection_model.h
new file mode 100644
index 0000000..5963d2a
--- /dev/null
+++ b/chrome/browser/tabs/tab_strip_selection_model.h
@@ -0,0 +1,104 @@
+// Copyright (c) 2011 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_TABS_TAB_STRIP_SELECTION_MODEL_H_
+#define CHROME_BROWSER_TABS_TAB_STRIP_SELECTION_MODEL_H_
+#pragma once
+
+#include <vector>
+
+#include "base/basictypes.h"
+
+// Selection model used by the tab strip. In addition to the set of selected
+// indices TabStripSelectionModel maintains the following:
+// active: the index of the currently visible tab in the tab strip.
+// anchor: the index of the last tab the user clicked on. Extending the
+// selection extends it from this index.
+//
+// Typically there is only one selected tab in the tabstrip, in which case the
+// anchor and active index correspond to the same thing.
+class TabStripSelectionModel {
+ public:
+ typedef std::vector<int> SelectedIndices;
+
+ // Used to identify no selection.
+ static const int kUnselectedIndex;
+
+ TabStripSelectionModel();
+ ~TabStripSelectionModel();
+
+ // See class description for details of the anchor.
+ void set_anchor(int anchor) { anchor_ = anchor; }
+ int anchor() const { return anchor_; }
+
+ // See class description for details of active.
+ void set_active(int active) { active_ = active; }
+ int active() const { return active_; }
+
+ // True if nothing is selected.
+ bool empty() const { return selected_indices_.empty(); }
+
+ // Number of selected indices.
+ size_t size() const { return selected_indices_.size(); }
+
+ // Increments all indices >= |index|. For example, if the selection consists
+ // of [0, 1, 5] and this is invoked with 1, it results in [0, 2, 6]. This also
+ // updates the anchor and active indices.
+ // This is used when a new tab is inserted into the tabstrip.
+ void IncrementFrom(int index);
+
+ // Shifts all indices < |index| down by 1. If |index| is selected, it is
+ // removed. For example, if the selection consists of [0, 1, 5] and this is
+ // invoked with 1, it results in [0, 4]. This is used when a tab is removed
+ // from the tabstrip.
+ void DecrementFrom(int index);
+
+ // Sets the anchor, active and selection to |index|.
+ void SetSelectedIndex(int index);
+
+ // Returns true if |index| is selected.
+ bool IsSelected(int index);
+
+ // Adds |index| to the selection. This does not change the active or anchor
+ // indices.
+ void AddIndexToSelection(int index);
+
+ // Removes |index| from the selection. This does not change the active or
+ // anchor indices.
+ void RemoveIndexFromSelection(int index);
+
+ // Extends the selection from the anchor to |index|. If the anchor is empty,
+ // this sets the anchor, selection and active indices to |index|.
+ void SetSelectionFromAnchorTo(int index);
+
+ // Invoked when an item moves. |from| is the original index, and |to| the
+ // target index.
+ // NOTE: this matches the TabStripModel API. If moving to a greater index,
+ // |to| should be the index *after* removing |from|. For example, consider
+ // three tabs 'A B C', to move A to the end of the list, this should be
+ // invoked with '0, 2'.
+ void Move(int from, int to);
+
+ // Sets the anchor and active to kUnselectedIndex, and removes all the
+ // selected indices.
+ void Clear();
+
+ // Returns the selected indices. The selection is always ordered in acending
+ // order.
+ const SelectedIndices& selected_indices() const { return selected_indices_; }
+
+ // Copies the selection from |source| to this.
+ void Copy(const TabStripSelectionModel& source);
+
+ private:
+ SelectedIndices selected_indices_;
+
+ int active_;
+
+ int anchor_;
+
+ DISALLOW_COPY_AND_ASSIGN(TabStripSelectionModel);
+};
+
+#endif // CHROME_BROWSER_TABS_TAB_STRIP_SELECTION_MODEL_H_
diff --git a/chrome/browser/tabs/tab_strip_selection_model_unittest.cc b/chrome/browser/tabs/tab_strip_selection_model_unittest.cc
new file mode 100644
index 0000000..78d898a
--- /dev/null
+++ b/chrome/browser/tabs/tab_strip_selection_model_unittest.cc
@@ -0,0 +1,141 @@
+// Copyright (c) 2011 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 <string>
+
+#include "base/string_number_conversions.h"
+#include "chrome/browser/tabs/tab_strip_selection_model.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+typedef testing::Test TabStripSelectionModelTest;
+
+// Returns the state of the selection model as a string. The format is:
+// 'active=X anchor=X selection=X X X...'.
+static std::string StateAsString(const TabStripSelectionModel& model) {
+ std::string result = "active=" + base::IntToString(model.active()) +
+ " anchor=" + base::IntToString(model.anchor()) +
+ " selection=";
+ const TabStripSelectionModel::SelectedIndices& selection(
+ model.selected_indices());
+ for (size_t i = 0; i < selection.size(); ++i) {
+ if (i != 0)
+ result += " ";
+ result += base::IntToString(selection[i]);
+ }
+ return result;
+}
+
+TEST_F(TabStripSelectionModelTest, InitialState) {
+ TabStripSelectionModel model;
+ EXPECT_EQ("active=-1 anchor=-1 selection=", StateAsString(model));
+ EXPECT_TRUE(model.empty());
+}
+
+TEST_F(TabStripSelectionModelTest, SetSelectedIndex) {
+ TabStripSelectionModel model;
+ model.SetSelectedIndex(2);
+ EXPECT_EQ("active=2 anchor=2 selection=2", StateAsString(model));
+ EXPECT_FALSE(model.empty());
+}
+
+TEST_F(TabStripSelectionModelTest, IncrementFrom) {
+ TabStripSelectionModel model;
+ model.SetSelectedIndex(1);
+ model.IncrementFrom(1);
+ EXPECT_EQ("active=2 anchor=2 selection=2", StateAsString(model));
+
+ // Increment from 4. This shouldn't effect the selection as its past the
+ // end of the selection.
+ model.IncrementFrom(4);
+ EXPECT_EQ("active=2 anchor=2 selection=2", StateAsString(model));
+}
+
+TEST_F(TabStripSelectionModelTest, DecrementFrom) {
+ TabStripSelectionModel model;
+ model.SetSelectedIndex(2);
+ model.DecrementFrom(0);
+ EXPECT_EQ("active=1 anchor=1 selection=1", StateAsString(model));
+
+ // Shift down from 1. As the selection as the index being removed, this should
+ // clear the selection.
+ model.DecrementFrom(1);
+ EXPECT_EQ("active=-1 anchor=-1 selection=", StateAsString(model));
+
+ // Reset the selection to 2, and shift down from 4. This shouldn't do
+ // anything.
+ model.SetSelectedIndex(2);
+ model.DecrementFrom(4);
+ EXPECT_EQ("active=2 anchor=2 selection=2", StateAsString(model));
+}
+
+TEST_F(TabStripSelectionModelTest, IsSelected) {
+ TabStripSelectionModel model;
+ model.SetSelectedIndex(2);
+ EXPECT_FALSE(model.IsSelected(0));
+ EXPECT_TRUE(model.IsSelected(2));
+}
+
+TEST_F(TabStripSelectionModelTest, AddIndexToSelected) {
+ TabStripSelectionModel model;
+ model.AddIndexToSelection(2);
+ EXPECT_EQ("active=-1 anchor=-1 selection=2", StateAsString(model));
+
+ model.AddIndexToSelection(4);
+ EXPECT_EQ("active=-1 anchor=-1 selection=2 4", StateAsString(model));
+}
+
+TEST_F(TabStripSelectionModelTest, RemoveIndexFromSelection) {
+ TabStripSelectionModel model;
+ model.SetSelectedIndex(2);
+ model.AddIndexToSelection(4);
+ EXPECT_EQ("active=2 anchor=2 selection=2 4", StateAsString(model));
+
+ model.RemoveIndexFromSelection(4);
+ EXPECT_EQ("active=2 anchor=2 selection=2", StateAsString(model));
+
+ model.RemoveIndexFromSelection(2);
+ EXPECT_EQ("active=2 anchor=2 selection=", StateAsString(model));
+}
+
+TEST_F(TabStripSelectionModelTest, Clear) {
+ TabStripSelectionModel model;
+ model.SetSelectedIndex(2);
+
+ model.Clear();
+ EXPECT_EQ("active=-1 anchor=-1 selection=", StateAsString(model));
+}
+
+TEST_F(TabStripSelectionModelTest, MoveToLeft) {
+ TabStripSelectionModel model;
+ model.SetSelectedIndex(0);
+ model.AddIndexToSelection(4);
+ model.AddIndexToSelection(10);
+ model.set_anchor(4);
+ model.set_active(4);
+ model.Move(4, 0);
+ EXPECT_EQ("active=0 anchor=0 selection=0 1 10", StateAsString(model));
+}
+
+TEST_F(TabStripSelectionModelTest, MoveToRight) {
+ TabStripSelectionModel model;
+ model.SetSelectedIndex(0);
+ model.AddIndexToSelection(4);
+ model.AddIndexToSelection(10);
+ model.set_anchor(0);
+ model.set_active(0);
+ model.Move(0, 3);
+ EXPECT_EQ("active=3 anchor=3 selection=3 4 10", StateAsString(model));
+}
+
+TEST_F(TabStripSelectionModelTest, Copy) {
+ TabStripSelectionModel model;
+ model.SetSelectedIndex(0);
+ model.AddIndexToSelection(4);
+ model.AddIndexToSelection(10);
+ EXPECT_EQ("active=0 anchor=0 selection=0 4 10", StateAsString(model));
+ TabStripSelectionModel model2;
+ model2.Copy(model);
+ EXPECT_EQ("active=0 anchor=0 selection=0 4 10", StateAsString(model2));
+}
diff --git a/chrome/chrome_browser.gypi b/chrome/chrome_browser.gypi
index ebbc8da..5469156 100644
--- a/chrome/chrome_browser.gypi
+++ b/chrome/chrome_browser.gypi
@@ -2010,6 +2010,8 @@
'browser/tabs/tab_strip_model_observer.h',
'browser/tabs/tab_strip_model_order_controller.cc',
'browser/tabs/tab_strip_model_order_controller.h',
+ 'browser/tabs/tab_strip_selection_model.cc',
+ 'browser/tabs/tab_strip_selection_model.h',
'browser/task_manager/task_manager.cc',
'browser/task_manager/task_manager.h',
'browser/task_manager/task_manager_resource_providers.cc',
diff --git a/chrome/chrome_tests.gypi b/chrome/chrome_tests.gypi
index cf55d7f..0255504 100644
--- a/chrome/chrome_tests.gypi
+++ b/chrome/chrome_tests.gypi
@@ -1520,6 +1520,7 @@
'browser/tab_contents/web_contents_unittest.cc',
'browser/tabs/pinned_tab_codec_unittest.cc',
'browser/tabs/tab_strip_model_unittest.cc',
+ 'browser/tabs/tab_strip_selection_model_unittest.cc',
'browser/task_manager/task_manager_unittest.cc',
'browser/themes/browser_theme_pack_unittest.cc',
'browser/themes/browser_theme_provider_unittest.cc',