diff options
author | sky@chromium.org <sky@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-03-09 19:47:01 +0000 |
---|---|---|
committer | sky@chromium.org <sky@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-03-09 19:47:01 +0000 |
commit | fe348bee89e025349af55425e6f7aa554b668891 (patch) | |
tree | af28cd5f32a8c04b386d59b63f6ad3ed5ac975db | |
parent | 9d1e2ba0d663c5f393410f71f3310b8e6a36747f (diff) | |
download | chromium_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.cc | 216 | ||||
-rw-r--r-- | chrome/browser/tabs/tab_strip_model.h | 52 | ||||
-rw-r--r-- | chrome/browser/tabs/tab_strip_model_unittest.cc | 86 | ||||
-rw-r--r-- | chrome/browser/tabs/tab_strip_selection_model.cc | 129 | ||||
-rw-r--r-- | chrome/browser/tabs/tab_strip_selection_model.h | 104 | ||||
-rw-r--r-- | chrome/browser/tabs/tab_strip_selection_model_unittest.cc | 141 | ||||
-rw-r--r-- | chrome/chrome_browser.gypi | 2 | ||||
-rw-r--r-- | chrome/chrome_tests.gypi | 1 |
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', |