summaryrefslogtreecommitdiffstats
path: root/chrome/browser/tabs
diff options
context:
space:
mode:
authorinitial.commit <initial.commit@0039d316-1c4b-4281-b951-d872f2087c98>2008-07-26 23:55:29 +0000
committerinitial.commit <initial.commit@0039d316-1c4b-4281-b951-d872f2087c98>2008-07-26 23:55:29 +0000
commit09911bf300f1a419907a9412154760efd0b7abc3 (patch)
treef131325fb4e2ad12c6d3504ab75b16dd92facfed /chrome/browser/tabs
parent586acc5fe142f498261f52c66862fa417c3d52d2 (diff)
downloadchromium_src-09911bf300f1a419907a9412154760efd0b7abc3.zip
chromium_src-09911bf300f1a419907a9412154760efd0b7abc3.tar.gz
chromium_src-09911bf300f1a419907a9412154760efd0b7abc3.tar.bz2
Add chrome to the repository.
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@15 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser/tabs')
-rw-r--r--chrome/browser/tabs/dragged_tab_controller.cc765
-rw-r--r--chrome/browser/tabs/dragged_tab_controller.h302
-rw-r--r--chrome/browser/tabs/dragged_tab_view.cc267
-rw-r--r--chrome/browser/tabs/dragged_tab_view.h143
-rw-r--r--chrome/browser/tabs/hwnd_photobooth.cc184
-rw-r--r--chrome/browser/tabs/hwnd_photobooth.h86
-rw-r--r--chrome/browser/tabs/tab.cc184
-rw-r--r--chrome/browser/tabs/tab.h134
-rw-r--r--chrome/browser/tabs/tab_renderer.cc691
-rw-r--r--chrome/browser/tabs/tab_renderer.h192
-rw-r--r--chrome/browser/tabs/tab_strip.cc1385
-rw-r--r--chrome/browser/tabs/tab_strip.h372
-rw-r--r--chrome/browser/tabs/tab_strip_model.cc593
-rw-r--r--chrome/browser/tabs/tab_strip_model.h536
-rw-r--r--chrome/browser/tabs/tab_strip_model_order_controller.cc146
-rw-r--r--chrome/browser/tabs/tab_strip_model_order_controller.h75
-rw-r--r--chrome/browser/tabs/tab_strip_model_unittest.cc1130
17 files changed, 7185 insertions, 0 deletions
diff --git a/chrome/browser/tabs/dragged_tab_controller.cc b/chrome/browser/tabs/dragged_tab_controller.cc
new file mode 100644
index 0000000..c5f2477
--- /dev/null
+++ b/chrome/browser/tabs/dragged_tab_controller.cc
@@ -0,0 +1,765 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include <math.h>
+
+#include "chrome/browser/tabs/dragged_tab_controller.h"
+
+#include "chrome/browser/chrome_frame.h"
+#include "chrome/browser/tab_contents.h"
+#include "chrome/browser/tabs/dragged_tab_view.h"
+#include "chrome/browser/tabs/hwnd_photobooth.h"
+#include "chrome/browser/tabs/tab.h"
+#include "chrome/browser/tabs/tab_strip.h"
+#include "chrome/browser/web_contents.h"
+#include "chrome/views/event.h"
+#include "skia/include/SkBitmap.h"
+
+static const int kSnapshotIntervalMs = 100;
+
+namespace {
+
+///////////////////////////////////////////////////////////////////////////////
+// WindowFinder
+// A WindowForPoint facility that can ignore 2 provided window HWNDs.
+//
+class WindowFinder {
+ public:
+ static HWND WindowForPoint(const gfx::Point& screen_point, HWND ignore1) {
+ WindowFinder instance(screen_point, ignore1);
+ return instance.GetResult();
+ }
+ private:
+ WindowFinder(const gfx::Point& screen_point, HWND ignore1)
+ : screen_point_(screen_point.ToPOINT()),
+ ignore1_(ignore1),
+ result_(NULL) {
+ }
+
+ static BOOL CALLBACK WindowEnumProc(HWND hwnd, LPARAM lParam) {
+ WindowFinder* wf = reinterpret_cast<WindowFinder*>(lParam);
+ if (hwnd == wf->ignore1_)
+ return true;
+
+ if (::IsWindowVisible(hwnd)) {
+ CRect r;
+ ::GetWindowRect(hwnd, &r);
+ if (r.PtInRect(wf->screen_point_)) {
+ // We always deal with the root HWND.
+ wf->result_ = GetAncestor(hwnd, GA_ROOT);
+ return FALSE;
+ }
+ }
+ return TRUE;
+ }
+
+ HWND GetResult() {
+ EnumThreadWindows(GetCurrentThreadId(), WindowEnumProc,
+ reinterpret_cast<LPARAM>(this));
+ return result_;
+ }
+
+ POINT screen_point_;
+ HWND ignore1_;
+ HWND result_;
+
+ DISALLOW_EVIL_CONSTRUCTORS(WindowFinder);
+};
+
+gfx::Point ConvertScreenPointToTabStripPoint(TabStrip* tabstrip,
+ const gfx::Point& screen_point) {
+ CPoint tabstrip_topleft(0, 0);
+ ChromeViews::View::ConvertPointToScreen(tabstrip, &tabstrip_topleft);
+ return gfx::Point(screen_point.x() - tabstrip_topleft.x,
+ screen_point.y() - tabstrip_topleft.y);
+}
+
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// DraggedTabController, public:
+
+DraggedTabController::DraggedTabController(Tab* source_tab,
+ TabStrip* source_tabstrip)
+ : dragged_contents_(NULL),
+ original_delegate_(NULL),
+ source_tab_(source_tab),
+ source_tabstrip_(source_tabstrip),
+ source_model_index_(source_tabstrip->GetIndexOfTab(source_tab)),
+ attached_tabstrip_(NULL),
+ old_focused_view_(NULL),
+ in_destructor_(false) {
+ ChangeDraggedContents(
+ source_tabstrip_->model()->GetTabContentsAt(source_model_index_));
+ // Listen for Esc key presses.
+ MessageLoop::current()->AddObserver(this);
+}
+
+DraggedTabController::~DraggedTabController() {
+ in_destructor_ = true;
+ MessageLoop::current()->RemoveObserver(this);
+ ChangeDraggedContents(NULL); // This removes our observer.
+}
+
+void DraggedTabController::CaptureDragInfo(const gfx::Point& mouse_offset) {
+ start_screen_point_ = GetCursorScreenPoint();
+ mouse_offset_ = mouse_offset;
+}
+
+void DraggedTabController::Drag() {
+ // Before we get to dragging anywhere, ensure that we consider ourselves
+ // attached to the source tabstrip.
+ if (source_tab_->IsVisible() && CanStartDrag())
+ Attach(source_tabstrip_, gfx::Point());
+
+ if (!source_tab_->IsVisible()) {
+ SaveFocus();
+ ContinueDragging();
+ }
+}
+
+void DraggedTabController::EndDrag(bool canceled) {
+ EndDragImpl(canceled ? CANCELED : NORMAL);
+}
+
+Tab* DraggedTabController::GetDragSourceTabForContents(
+ TabContents* contents) const {
+ return contents == dragged_contents_ ? source_tab_ : NULL;
+}
+
+bool DraggedTabController::IsDragSourceTab(Tab* tab) const {
+ return source_tab_ == tab;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// DraggedTabController, PageNavigator implementation:
+
+void DraggedTabController::OpenURLFromTab(TabContents* source,
+ const GURL& url,
+ WindowOpenDisposition disposition,
+ PageTransition::Type transition) {
+ if (original_delegate_) {
+ if (disposition == CURRENT_TAB)
+ disposition = NEW_WINDOW;
+
+ original_delegate_->OpenURLFromTab(source, url, disposition, transition);
+ }
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// DraggedTabController, TabContentsDelegate implementation:
+
+void DraggedTabController::NavigationStateChanged(const TabContents* source,
+ unsigned changed_flags) {
+ if (view_.get())
+ view_->Update();
+}
+
+void DraggedTabController::ReplaceContents(TabContents* source,
+ TabContents* new_contents) {
+ DCHECK(dragged_contents_ == source);
+ source->set_delegate(NULL);
+ new_contents->set_delegate(this);
+
+ // If we're attached to a TabStrip, we need to tell the TabStrip that this
+ // TabContents was replaced.
+ if (attached_tabstrip_ && attached_tabstrip_->model() && dragged_contents_) {
+ int index =
+ attached_tabstrip_->model()->GetIndexOfTabContents(dragged_contents_);
+ if (index != TabStripModel::kNoTab)
+ attached_tabstrip_->model()->ReplaceTabContentsAt(index, new_contents);
+ }
+
+ // Update our internal state.
+ ChangeDraggedContents(new_contents);
+
+ if (view_.get())
+ view_->Update();
+}
+
+void DraggedTabController::AddNewContents(TabContents* source,
+ TabContents* new_contents,
+ WindowOpenDisposition disposition,
+ const gfx::Rect& initial_pos,
+ bool user_gesture) {
+ // Theoretically could be called while dragging if the page tries to
+ // spawn a window. Route this message back to the browser in most cases.
+ if (disposition == CURRENT_TAB) {
+ ReplaceContents(source, new_contents);
+ } else if (original_delegate_) {
+ original_delegate_->AddNewContents(source, new_contents, disposition,
+ initial_pos, user_gesture);
+ }
+}
+
+void DraggedTabController::ActivateContents(TabContents* contents) {
+ // Ignored.
+}
+
+void DraggedTabController::LoadingStateChanged(TabContents* source) {
+ // It would be nice to respond to this message by changing the
+ // screen shot in the dragged tab.
+ if (view_.get())
+ view_->Update();
+}
+
+void DraggedTabController::CloseContents(TabContents* source) {
+ // Theoretically could be called by a window. Should be ignored
+ // because window.close() is ignored (usually, even though this
+ // method gets called.)
+}
+
+void DraggedTabController::MoveContents(TabContents* source,
+ const gfx::Rect& pos) {
+ // Theoretically could be called by a web page trying to move its
+ // own window. Should be ignored since we're moving the window...
+}
+
+bool DraggedTabController::IsPopup(TabContents* source) {
+ return false;
+}
+
+void DraggedTabController::ToolbarSizeChanged(TabContents* source,
+ bool finished) {
+ // Dragged tabs don't care about this.
+}
+
+void DraggedTabController::URLStarredChanged(TabContents* source,
+ bool starred) {
+ // Ignored.
+}
+
+void DraggedTabController::UpdateTargetURL(TabContents* source,
+ const GURL& url) {
+ // Ignored.
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// DraggedTabController, NotificationObserver implementation:
+
+void DraggedTabController::Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details) {
+ DCHECK(type == NOTIFY_TAB_CONTENTS_DESTROYED);
+ DCHECK(Source<TabContents>(source).ptr() == dragged_contents_);
+ EndDragImpl(TAB_DESTROYED);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// DraggedTabController, MessageLoop::Observer implementation:
+
+void DraggedTabController::WillProcessMessage(const MSG& msg) {
+}
+
+void DraggedTabController::DidProcessMessage(const MSG& msg) {
+ // If the user presses ESC during a drag, we need to abort and revert things
+ // to the way they were. This is the most reliable way to do this since no
+ // single view or window reliably receives events throughout all the various
+ // kinds of tab dragging.
+ if (msg.message == WM_KEYDOWN && msg.wParam == VK_ESCAPE)
+ EndDrag(true);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// DraggedTabController, private:
+
+void DraggedTabController::InitWindowCreatePoint() {
+ CPoint mouse_offset_cpoint(mouse_offset_.x(), mouse_offset_.y());
+ Tab* first_tab = attached_tabstrip_->GetTabAt(0);
+ ChromeViews::View::ConvertPointToViewContainer(first_tab,
+ &mouse_offset_cpoint);
+ window_create_point_.SetPoint(mouse_offset_cpoint.x, mouse_offset_cpoint.y);
+}
+
+gfx::Point DraggedTabController::GetWindowCreatePoint() const {
+ POINT pt;
+ GetCursorPos(&pt);
+ return gfx::Point(pt.x - window_create_point_.x(),
+ pt.y - window_create_point_.y());
+}
+
+void DraggedTabController::ChangeDraggedContents(TabContents* new_contents) {
+ if (dragged_contents_) {
+ NotificationService::current()->RemoveObserver(this,
+ NOTIFY_TAB_CONTENTS_DESTROYED,
+ Source<TabContents>(dragged_contents_));
+ }
+ dragged_contents_ = new_contents;
+ if (dragged_contents_) {
+ NotificationService::current()->AddObserver(this,
+ NOTIFY_TAB_CONTENTS_DESTROYED,
+ Source<TabContents>(dragged_contents_));
+ }
+}
+
+void DraggedTabController::SaveFocus() {
+ if (!old_focused_view_) {
+ old_focused_view_ = source_tab_->GetRootView()->GetFocusedView();
+ source_tab_->GetRootView()->FocusView(source_tab_);
+ }
+}
+
+void DraggedTabController::RestoreFocus() {
+ if (old_focused_view_ && attached_tabstrip_ == source_tabstrip_)
+ old_focused_view_->GetRootView()->FocusView(old_focused_view_);
+ old_focused_view_ = NULL;
+}
+
+bool DraggedTabController::CanStartDrag() const {
+ // Determine if the mouse has moved beyond a minimum elasticity distance in
+ // any direction from the starting point.
+ static const int kMinimumDragDistance = 10;
+ gfx::Point screen_point = GetCursorScreenPoint();
+ int x_offset = abs(screen_point.x() - start_screen_point_.x());
+ int y_offset = abs(screen_point.y() - start_screen_point_.y());
+ return sqrt(pow(static_cast<float>(x_offset), 2) +
+ pow(static_cast<float>(y_offset), 2)) > kMinimumDragDistance;
+}
+
+void DraggedTabController::ContinueDragging() {
+ EnsureDraggedView();
+
+ // Note that the coordinates given to us by |drag_event| are basically
+ // useless, since they're in source_tab_ coordinates. On the surface, you'd
+ // think we could just convert them to screen coordinates, however in the
+ // situation where we're dragging the last tab in a window when multiple
+ // windows are open, the coordinates of |source_tab_| are way off in
+ // hyperspace since the window was moved there instead of being closed so
+ // that we'd keep receiving events. And our ConvertPointToScreen methods
+ // aren't really multi-screen aware. So really it's just safer to get the
+ // actual position of the mouse cursor directly from Windows here, which is
+ // guaranteed to be correct regardless of monitor config.
+ gfx::Point screen_point = GetCursorScreenPoint();
+
+ // Determine whether or not we have dragged over a compatible TabStrip in
+ // another browser window. If we have, we should attach to it and start
+ // dragging within it.
+ TabStrip* target_tabstrip = GetTabStripForPoint(screen_point);
+ if (target_tabstrip != attached_tabstrip_) {
+ if (target_tabstrip) {
+ // We may receive this event before we're fully detached from the source,
+ // we check for that and force a detach now.
+ if (attached_tabstrip_)
+ Detach();
+ Attach(target_tabstrip, screen_point);
+ } else {
+ Detach();
+ }
+ }
+ MoveTab(screen_point);
+}
+
+void DraggedTabController::MoveTab(const gfx::Point& screen_point) {
+ gfx::Point dragged_view_point = GetDraggedViewPoint(screen_point);
+ if (attached_tabstrip_) {
+ TabStripModel* attached_model = attached_tabstrip_->model();
+ int from_index = attached_model->GetIndexOfTabContents(dragged_contents_);
+ gfx::Rect bounds = GetDraggedViewTabStripBounds(dragged_view_point);
+ int to_index = GetInsertionIndexForDraggedBounds(bounds);
+ to_index = NormalizeIndexToAttachedTabStrip(to_index);
+ attached_model->MoveTabContentsAt(from_index, to_index);
+ }
+ view_->MoveTo(dragged_view_point);
+}
+
+TabStrip* DraggedTabController::GetTabStripForPoint(
+ const gfx::Point& screen_point) const {
+ HWND dragged_hwnd = view_->GetViewContainer()->GetHWND();
+ HWND other_hwnd = WindowFinder::WindowForPoint(screen_point, dragged_hwnd);
+ if (!other_hwnd)
+ return NULL;
+
+ ChromeFrame* other_frame = ChromeFrame::GetChromeFrameForWindow(other_hwnd);
+ if (other_frame) {
+ TabStrip* other_tabstrip = other_frame->GetTabStrip();
+ if (!other_tabstrip->IsCompatibleWith(source_tabstrip_))
+ return NULL;
+ return GetTabStripIfItContains(other_tabstrip, screen_point);
+ }
+ return NULL;
+}
+
+TabStrip* DraggedTabController::GetTabStripIfItContains(
+ TabStrip* tabstrip, const gfx::Point& screen_point) const {
+ static const int kVerticalDetachMagnetism = 15;
+ // Make sure the specified screen point is actually within the bounds of the
+ // specified tabstrip...
+ gfx::Rect tabstrip_bounds = GetViewScreenBounds(tabstrip);
+ if (screen_point.x() < tabstrip_bounds.right() &&
+ screen_point.x() >= tabstrip_bounds.x()) {
+ // TODO(beng): make this be relative to the start position of the mouse for
+ // the source TabStrip.
+ int upper_threshold = tabstrip_bounds.bottom() + kVerticalDetachMagnetism;
+ int lower_threshold = tabstrip_bounds.y() - kVerticalDetachMagnetism;
+ if (screen_point.y() >= lower_threshold &&
+ screen_point.y() <= upper_threshold) {
+ return tabstrip;
+ }
+ }
+ return NULL;
+}
+
+void DraggedTabController::Attach(TabStrip* attached_tabstrip,
+ const gfx::Point& screen_point) {
+ attached_tabstrip_ = attached_tabstrip;
+ InitWindowCreatePoint();
+
+ // We don't need the photo-booth while we're attached.
+ photobooth_.reset(NULL);
+
+ Tab* tab = GetTabMatchingDraggedContents(attached_tabstrip_);
+
+ // Update the View first, so we can ask it for its bounds and determine
+ // where to insert the hidden Tab.
+
+ // If this is the first time Attach is called for this drag, we're attaching
+ // to the source TabStrip, and we should assume the tab count already
+ // includes this Tab since we haven't been detached yet. If we don't do this,
+ // the dragged representation will be a different size to others in the
+ // TabStrip.
+ int tab_count = attached_tabstrip_->GetTabCount();
+ if (!tab)
+ ++tab_count;
+ double unselected_width, selected_width = 0;
+ attached_tabstrip_->GetDesiredTabWidths(tab_count, &unselected_width,
+ &selected_width);
+ EnsureDraggedView();
+ view_->Attach(static_cast<int>(selected_width));
+
+ if (!tab) {
+ // There is no Tab in |attached_tabstrip| that corresponds to the dragged
+ // TabContents. We must now create one.
+
+ // Remove ourselves as the delegate now that the dragged TabContents is
+ // being inserted back into a Browser.
+ dragged_contents_->set_delegate(NULL);
+ original_delegate_ = NULL;
+
+ // Return the TabContents' to normalcy.
+ dragged_contents_->DidCaptureContents();
+
+ gfx::Rect bounds = GetDraggedViewTabStripBounds(screen_point);
+ int index = GetInsertionIndexForDraggedBounds(bounds);
+ index = NormalizeIndexToAttachedTabStrip(index);
+ attached_tabstrip_->model()->InsertTabContentsAt(index, dragged_contents_,
+ true, false);
+
+ tab = GetTabMatchingDraggedContents(attached_tabstrip_);
+ }
+ DCHECK(tab); // We should now have a tab.
+ tab->SetVisible(false);
+
+ // Move the corresponding window to the front.
+ attached_tabstrip_->GetViewContainer()->MoveToFront(true);
+}
+
+void DraggedTabController::Detach() {
+ // Prevent the TabContents' HWND from being hidden by any of the model
+ // operations performed during the drag.
+ dragged_contents_->WillCaptureContents();
+
+ // Update the Model.
+ TabStripModel* attached_model = attached_tabstrip_->model();
+ int index = attached_model->GetIndexOfTabContents(dragged_contents_);
+ if (index >= 0 && index < attached_model->count()) {
+ attached_model->DetachTabContentsAt(index);
+ attached_tabstrip_->SchedulePaint();
+ }
+
+ // If we've removed the last Tab from the TabStrip, hide the frame now.
+ if (attached_model->empty())
+ HideFrame();
+
+ // Set up the photo booth to start capturing the contents of the dragged
+ // TabContents.
+ if (!photobooth_.get())
+ photobooth_.reset(new HWNDPhotobooth(dragged_contents_->GetContainerHWND()));
+
+ // Update the View.
+ view_->Detach(photobooth_.get());
+
+ // We need to be the delegate so we receive messages about stuff,
+ // otherwise our dragged_contents() may be replaced and subsequently
+ // collected/destroyed while the drag is in process, leading to
+ // nasty crashes.
+ original_delegate_ = dragged_contents_->delegate();
+ dragged_contents_->set_delegate(this);
+
+ attached_tabstrip_ = NULL;
+}
+
+int DraggedTabController::GetInsertionIndexForDraggedBounds(
+ const gfx::Rect& dragged_bounds) const {
+ int right_tab_x = 0;
+
+ // If the UI layout of the tab strip is right-to-left, we need to mirror the
+ // bounds of the dragged tab before performing the drag/drop related
+ // calculations. We mirror the dragged bounds because we determine the
+ // position of each tab on the tab strip by calling GetBounds() (without the
+ // mirroring transformation flag) which effectively means that even though
+ // the tabs are rendered from right to left, the code performs the
+ // calculation as if the tabs are laid out from left to right. Mirroring the
+ // dragged bounds adjusts the coordinates of the tab we are dragging so that
+ // it uses the same orientation used by the tabs on the tab strip.
+ gfx::Rect adjusted_bounds(dragged_bounds);
+ adjusted_bounds.set_x(
+ attached_tabstrip_->MirroredLeftPointForRect(adjusted_bounds));
+
+ for (int i = 0; i < attached_tabstrip_->GetTabCount(); ++i) {
+ gfx::Rect ideal_bounds = attached_tabstrip_->GetIdealBounds(i);
+ gfx::Rect left_half = ideal_bounds;
+ left_half.set_width(left_half.width() / 2);
+ gfx::Rect right_half = ideal_bounds;
+ right_half.set_width(ideal_bounds.width() - left_half.width());
+ right_half.set_x(left_half.right());
+ right_tab_x = right_half.right();
+ if (adjusted_bounds.x() >= right_half.x() &&
+ adjusted_bounds.x() < right_half.right()) {
+ return i + 1;
+ } else if (adjusted_bounds.x() >= left_half.x() &&
+ adjusted_bounds.x() < left_half.right()) {
+ return i;
+ }
+ }
+ if (adjusted_bounds.right() > right_tab_x)
+ return attached_tabstrip_->model()->count();
+ return TabStripModel::kNoTab;
+}
+
+gfx::Rect DraggedTabController::GetDraggedViewTabStripBounds(
+ const gfx::Point& screen_point) {
+ gfx::Point client_point =
+ ConvertScreenPointToTabStripPoint(attached_tabstrip_, screen_point);
+ gfx::Size view_size = view_->attached_tab_size();
+ return gfx::Rect(client_point.x(), client_point.y(),
+ view_size.width(), view_size.height());
+}
+
+gfx::Point DraggedTabController::GetDraggedViewPoint(
+ const gfx::Point& screen_point) {
+ int x = screen_point.x() - mouse_offset_.x();
+ int y = screen_point.y() - mouse_offset_.y();
+
+ // If we're not attached, we just use x and y from above.
+ if (attached_tabstrip_) {
+ gfx::Rect tabstrip_bounds = GetViewScreenBounds(attached_tabstrip_);
+ // Snap the dragged Tab to the TabStrip if we are attached, detaching
+ // only when the mouse position (screen_point) exceeds the screen bounds
+ // of the TabStrip.
+ if (x < tabstrip_bounds.x() && screen_point.x() >= tabstrip_bounds.x())
+ x = tabstrip_bounds.x();
+
+ gfx::Size tab_size = view_->attached_tab_size();
+ int vertical_drag_magnetism = tab_size.height() * 2;
+ int vertical_detach_point = tabstrip_bounds.y() - vertical_drag_magnetism;
+ if (y < tabstrip_bounds.y() && screen_point.y() >= vertical_detach_point)
+ y = tabstrip_bounds.y();
+
+ // Make sure the Tab can't be dragged off the right side of the TabStrip
+ // unless the mouse pointer passes outside the bounds of the strip by
+ // clamping the position of the dragged window to the tabstrip width less
+ // the width of one tab until the mouse pointer (screen_point) exceeds the
+ // screen bounds of the TabStrip.
+ int max_x = tabstrip_bounds.right() - tab_size.width();
+ int max_y = tabstrip_bounds.bottom() - tab_size.height();
+ if (x > max_x && screen_point.x() <= tabstrip_bounds.right())
+ x = max_x;
+ if (y > max_y && screen_point.y() <=
+ (tabstrip_bounds.bottom() + vertical_drag_magnetism)) {
+ y = max_y;
+ }
+ }
+ return gfx::Point(x, y);
+}
+
+
+Tab* DraggedTabController::GetTabMatchingDraggedContents(
+ TabStrip* tabstrip) const {
+ int index = tabstrip->model()->GetIndexOfTabContents(dragged_contents_);
+ return index == TabStripModel::kNoTab ? NULL : tabstrip->GetTabAt(index);
+}
+
+void DraggedTabController::EndDragImpl(EndDragType type) {
+ bool destroy_now = true;
+ if (type != TAB_DESTROYED) {
+ // We only finish up the drag if we were actually dragging. If we never
+ // constructed a view, the user just clicked and released and didn't move the
+ // mouse enough to trigger a drag.
+ if (view_.get()) {
+ RestoreFocus();
+ if (type == CANCELED) {
+ RevertDrag();
+ } else {
+ destroy_now = CompleteDrag();
+ }
+ }
+ } else {
+ // If we get here it means the NavigationController is going down. Don't
+ // attempt to do any cleanup other than resetting the delegate (if we're
+ // still the delegate).
+ if (dragged_contents_ && dragged_contents_->delegate() == this)
+ dragged_contents_->set_delegate(NULL);
+ dragged_contents_ = NULL;
+ attached_tabstrip_ = NULL;
+ }
+ // If we're not destroyed now, we'll be destroyed asynchronously later.
+ if (destroy_now)
+ source_tabstrip_->DestroyDragController();
+}
+
+void DraggedTabController::RevertDrag() {
+ if (attached_tabstrip_) {
+ int index = attached_tabstrip_->model()->GetIndexOfTabContents(
+ dragged_contents_);
+ if (attached_tabstrip_ != source_tabstrip_) {
+ // The Tab was inserted into another TabStrip. We need to put it back
+ // into the original one.
+ attached_tabstrip_->model()->DetachTabContentsAt(index);
+ source_tabstrip_->model()->InsertTabContentsAt(source_model_index_,
+ dragged_contents_, true, false);
+ } else {
+ // The Tab was moved within the TabStrip where the drag was initiated.
+ // Move it back to the starting location.
+ source_tabstrip_->model()->MoveTabContentsAt(index, source_model_index_);
+ }
+ } else {
+ // The Tab was detached from the TabStrip where the drag began, and has not
+ // been attached to any other TabStrip. We need to put it back into the
+ // source TabStrip.
+ source_tabstrip_->model()->InsertTabContentsAt(source_model_index_,
+ dragged_contents_, true, false);
+ }
+ // If we're not attached to any TabStrip, or attached to some other TabStrip,
+ // we need to restore the bounds of the original TabStrip's frame, in case
+ // it has been hidden.
+ if (!attached_tabstrip_ || attached_tabstrip_ != source_tabstrip_) {
+ if (!restore_bounds_.IsEmpty()) {
+ HWND frame_hwnd = source_tabstrip_->GetViewContainer()->GetHWND();
+ MoveWindow(frame_hwnd, restore_bounds_.x(), restore_bounds_.y(),
+ restore_bounds_.width(), restore_bounds_.height(), TRUE);
+ }
+ }
+ source_tab_->SetVisible(true);
+}
+
+bool DraggedTabController::CompleteDrag() {
+ bool destroy_immediately = true;
+ if (attached_tabstrip_) {
+ // We don't need to do anything other than make the Tab visible again,
+ // since the dragged View is going away.
+ Tab* tab = GetTabMatchingDraggedContents(attached_tabstrip_);
+ view_->AnimateToBounds(
+ GetViewScreenBounds(tab),
+ NewCallback(this, &DraggedTabController::OnAnimateToBoundsComplete));
+ destroy_immediately = false;
+ } else {
+ // Compel the model to construct a new window for the detached TabContents.
+ // TODO(beng): this is here for one cycle only to see if it's
+ // dragged_contents_ that's getting freed, or something else.
+ dragged_contents_->GetContentHWND();
+ dragged_contents_->GetTitle().size();
+ source_tabstrip_->model()->TearOffTabContents(
+ dragged_contents_,
+ GetWindowCreatePoint());
+ CleanUpHiddenFrame();
+ }
+
+ return destroy_immediately;
+}
+
+void DraggedTabController::EnsureDraggedView() {
+ if (!view_.get()) {
+ RECT wr;
+ GetWindowRect(dragged_contents_->GetContainerHWND(), &wr);
+
+ view_.reset(new DraggedTabView(dragged_contents_, mouse_offset_,
+ gfx::Size(wr.right - wr.left, wr.bottom - wr.top)));
+ }
+}
+
+gfx::Point DraggedTabController::GetCursorScreenPoint() const {
+ POINT pt;
+ GetCursorPos(&pt);
+ return gfx::Point(pt);
+}
+
+gfx::Rect DraggedTabController::GetViewScreenBounds(
+ ChromeViews::View* view) const {
+ CPoint view_topleft(0, 0);
+ ChromeViews::View::ConvertPointToScreen(view, &view_topleft);
+ CRect view_screen_bounds;
+ view->GetLocalBounds(&view_screen_bounds, true);
+ view_screen_bounds.OffsetRect(view_topleft);
+ return gfx::Rect(view_screen_bounds);
+}
+
+int DraggedTabController::NormalizeIndexToAttachedTabStrip(int index) const {
+ DCHECK(attached_tabstrip_) << "Can only be called when attached!";
+ TabStripModel* attached_model = attached_tabstrip_->model();
+ if (index >= attached_model->count())
+ return attached_model->count() - 1;
+ if (index == TabStripModel::kNoTab)
+ return 0;
+ return index;
+}
+
+void DraggedTabController::HideFrame() {
+ // We don't actually hide the window, rather we just move it way off-screen.
+ // If we actually hide it, we stop receiving drag events.
+ HWND frame_hwnd = source_tabstrip_->GetViewContainer()->GetHWND();
+ RECT wr;
+ GetWindowRect(frame_hwnd, &wr);
+ MoveWindow(frame_hwnd, 0xFFFF, 0xFFFF, wr.right - wr.left,
+ wr.bottom - wr.top, TRUE);
+
+ // We also save the bounds of the window prior to it being moved, so that if
+ // the drag session is aborted we can restore them.
+ restore_bounds_ = gfx::Rect(wr);
+}
+
+void DraggedTabController::CleanUpHiddenFrame() {
+ // If the model we started dragging from is now empty, we must ask the
+ // delegate to close the frame.
+ if (source_tabstrip_->model()->empty())
+ source_tabstrip_->model()->delegate()->CloseFrameAfterDragSession();
+}
+
+void DraggedTabController::OnAnimateToBoundsComplete() {
+ // Sometimes, for some reason, in automation we can be called back on a
+ // detach even though we aren't attached to a TabStrip. Guard against that.
+ if (attached_tabstrip_) {
+ Tab* tab = GetTabMatchingDraggedContents(attached_tabstrip_);
+ if (tab)
+ tab->SetVisible(true);
+ }
+ CleanUpHiddenFrame();
+
+ if (!in_destructor_)
+ source_tabstrip_->DestroyDragController();
+}
diff --git a/chrome/browser/tabs/dragged_tab_controller.h b/chrome/browser/tabs/dragged_tab_controller.h
new file mode 100644
index 0000000..610bb92
--- /dev/null
+++ b/chrome/browser/tabs/dragged_tab_controller.h
@@ -0,0 +1,302 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef CHROME_BROWSER_TABS_DRAGGED_TAB_CONTROLLER_H__
+#define CHROME_BROWSER_TABS_DRAGGED_TAB_CONTROLLER_H__
+
+#include "base/gfx/point.h"
+#include "base/gfx/rect.h"
+#include "chrome/browser/tab_contents_delegate.h"
+#include "chrome/browser/tabs/tab_renderer.h"
+#include "chrome/common/notification_service.h"
+
+namespace ChromeViews {
+class MouseEvent;
+class View;
+}
+class DraggedTabView;
+class HWNDPhotobooth;
+class SkBitmap;
+class Tab;
+class TabStrip;
+class TabStripModel;
+
+///////////////////////////////////////////////////////////////////////////////
+//
+// DraggedTabController
+//
+// An object that handles a drag session for an individual Tab within a
+// TabStrip. This object is created whenever the mouse is pressed down on a
+// Tab and destroyed when the mouse is released or the drag operation is
+// aborted. The Tab that the user dragged (the "source tab") owns this object
+// and must be the only one to destroy it (via |DestroyDragController|).
+//
+///////////////////////////////////////////////////////////////////////////////
+class DraggedTabController : public TabContentsDelegate,
+ public NotificationObserver,
+ public MessageLoop::Observer{
+ public:
+ DraggedTabController(Tab* source_tab, TabStrip* source_tabstrip);
+ virtual ~DraggedTabController();
+
+ // Capture information needed to be used during a drag session for this
+ // controller's associated source Tab and TabStrip. |mouse_offset| is the
+ // distance of the mouse pointer from the Tab's origin.
+ void CaptureDragInfo(const gfx::Point& mouse_offset);
+
+ // Responds to drag events subsequent to StartDrag. If the mouse moves a
+ // sufficient distance before the mouse is released, a drag session is
+ // initiated.
+ void Drag();
+
+ // Complete the current drag session. If the drag session was canceled
+ // because the user pressed Escape or something interrupted it, |canceled|
+ // is true so the helper can revert the state to the world before the drag
+ // begun.
+ void EndDrag(bool canceled);
+
+ // Retrieve the source Tab if the TabContents specified matches the one being
+ // dragged by this controller, or NULL if the specified TabContents is not
+ // the same as the one being dragged.
+ Tab* GetDragSourceTabForContents(TabContents* contents) const;
+
+ // Returns true if the specified Tab matches the Tab being dragged.
+ bool IsDragSourceTab(Tab* tab) const;
+
+ private:
+ // Enumeration of the ways a drag session can end.
+ enum EndDragType {
+ // Drag session exited normally: the user released the mouse.
+ NORMAL,
+
+ // The drag session was canceled (alt-tab during drag, escape ...)
+ CANCELED,
+
+ // The tab (NavigationController) was destroyed during the drag.
+ TAB_DESTROYED
+ };
+
+ // Overridden from TabContentsDelegate:
+ virtual void OpenURLFromTab(TabContents* source,
+ const GURL& url,
+ WindowOpenDisposition disposition,
+ PageTransition::Type transition);
+ virtual void NavigationStateChanged(const TabContents* source,
+ unsigned changed_flags);
+ virtual void ReplaceContents(TabContents* source,
+ TabContents* new_contents);
+ virtual void AddNewContents(TabContents* source,
+ TabContents* new_contents,
+ WindowOpenDisposition disposition,
+ const gfx::Rect& initial_pos,
+ bool user_gesture);
+ virtual void ActivateContents(TabContents* contents);
+ virtual void LoadingStateChanged(TabContents* source);
+ virtual void CloseContents(TabContents* source);
+ virtual void MoveContents(TabContents* source, const gfx::Rect& pos);
+ virtual bool IsPopup(TabContents* source);
+ virtual void ToolbarSizeChanged(TabContents* source, bool is_animating);
+ virtual void URLStarredChanged(TabContents* source, bool starred);
+ virtual void UpdateTargetURL(TabContents* source, const GURL& url);
+
+ // Overridden from NotificationObserver:
+ virtual void Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details);
+
+ // Overridden from MessageLoop::Observer:
+ virtual void WillProcessMessage(const MSG& msg);
+ virtual void DidProcessMessage(const MSG& msg);
+
+ // Initialize the offset used to calculate the position to create windows
+ // in |GetWindowCreatePoint|.
+ void InitWindowCreatePoint();
+
+ // Returns the point where a detached window should be created given the
+ // current mouse position.
+ gfx::Point GetWindowCreatePoint() const;
+
+ // Replaces the TabContents being dragged with the specified |new_contents|.
+ // This can occur if the active TabContents for the tab being dragged is
+ // replaced, e.g. if a transition from one TabContentsType to another occurs
+ // during the drag.
+ void ChangeDraggedContents(TabContents* new_contents);
+
+ // Saves focus in the window that the drag initiated from. Focus will be
+ // restored appropriately if the drag ends within this same window.
+ void SaveFocus();
+
+ // Restore focus to the View that had focus before the drag was started, if
+ // the drag ends within the same Window as it began.
+ void RestoreFocus();
+
+ // Tests whether the position of the mouse is past a minimum elasticity
+ // threshold required to start a drag.
+ bool CanStartDrag() const;
+
+ // Move the DraggedTabView according to the current mouse screen position,
+ // potentially updating the source and other TabStrips.
+ void ContinueDragging();
+
+ // Handles moving the Tab within a TabStrip as well as updating the View.
+ void MoveTab(const gfx::Point& screen_point);
+
+ // Returns the compatible TabStrip that is under the specified point (screen
+ // coordinates), or NULL if there is none.
+ TabStrip* GetTabStripForPoint(const gfx::Point& screen_point) const;
+
+ // Returns the specified |tabstrip| if it contains the specified point
+ // (screen coordinates), NULL if it does not.
+ TabStrip* GetTabStripIfItContains(TabStrip* tabstrip,
+ const gfx::Point& screen_point) const;
+
+ // Attach the dragged Tab to the specified TabStrip.
+ void Attach(TabStrip* attached_tabstrip, const gfx::Point& screen_point);
+
+ // Detach the dragged Tab from the current TabStrip.
+ void Detach();
+
+ // Returns the index where the dragged TabContents should be inserted into
+ // the attached TabStripModel given the DraggedTabView's bounds
+ // |dragged_bounds| in coordinates relative to the attached TabStrip.
+ int GetInsertionIndexForDraggedBounds(const gfx::Rect& dragged_bounds) const;
+
+ // Retrieve the bounds of the DraggedTabView, relative to the attached
+ // TabStrip, given location of the dragged tab in screen coordinates.
+ gfx::Rect GetDraggedViewTabStripBounds(const gfx::Point& screen_point);
+
+ // Get the position of the dragged tab view relative to the attached tab
+ // strip.
+ gfx::Point GetDraggedViewPoint(const gfx::Point& screen_point);
+
+ // Finds the Tab within the specified TabStrip that corresponds to the
+ // dragged TabContents.
+ Tab* GetTabMatchingDraggedContents(TabStrip* tabstrip) const;
+
+ // Does the work for EndDrag.
+ void EndDragImpl(EndDragType how_end);
+
+ // If the drag was aborted for some reason, this function is called to un-do
+ // the changes made during the drag operation.
+ void RevertDrag();
+
+ // Finishes the drag operation. Returns true if the drag controller should
+ // be destroyed immediately, false otherwise.
+ bool CompleteDrag();
+
+ // Create the DraggedTabView, if it does not yet exist.
+ void EnsureDraggedView();
+
+ // Utility for getting the mouse position in screen coordinates.
+ gfx::Point GetCursorScreenPoint() const;
+
+ // Returns the bounds (in screen coordinates) of the specified View.
+ gfx::Rect GetViewScreenBounds(ChromeViews::View* tabstrip) const;
+
+ // Utility to convert the specified TabStripModel index to something valid
+ // for the attached TabStrip.
+ int NormalizeIndexToAttachedTabStrip(int index) const;
+
+ // Hides the frame for the window that contains the TabStrip the current
+ // drag session was initiated from.
+ void HideFrame();
+
+ // Closes a hidden frame at the end of a drag session.
+ void CleanUpHiddenFrame();
+
+ // Completes the drag session after the view has animated to its final
+ // position.
+ void OnAnimateToBoundsComplete();
+
+ // The TabContents being dragged. This can get replaced during the drag if
+ // the associated NavigationController is navigated to a different
+ // TabContentsType.
+ TabContents* dragged_contents_;
+
+ // The original TabContentsDelegate of |dragged_contents_|, before it was
+ // detached from the browser window. We store this so that we can forward
+ // certain delegate notifications back to it if we can't handle them locally.
+ TabContentsDelegate* original_delegate_;
+
+ // The Tab that initiated the drag session.
+ Tab* source_tab_;
+
+ // The TabStrip |source_tab_| originated from.
+ TabStrip* source_tabstrip_;
+
+ // This is the index of the |source_tab_| in |source_tabstrip_| when the drag
+ // began. This is used to restore the previous state if the drag is aborted.
+ int source_model_index_;
+
+ // The TabStrip the dragged Tab is currently attached to, or NULL if the
+ // dragged Tab is detached.
+ TabStrip* attached_tabstrip_;
+
+ // The visual representation of the dragged Tab.
+ scoped_ptr<DraggedTabView> view_;
+
+ // The photo-booth the TabContents sits in when the Tab is detached, to
+ // obtain screen shots.
+ scoped_ptr<HWNDPhotobooth> photobooth_;
+
+ // The position of the mouse (in screen coordinates) at the start of the drag
+ // operation. This is used to calculate minimum elasticity before a
+ // DraggedTabView is constructed.
+ gfx::Point start_screen_point_;
+
+ // This is the offset of the mouse from the top left of the Tab where
+ // dragging begun. This is used to ensure that the dragged view is always
+ // positioned at the correct location during the drag, and to ensure that the
+ // detached window is created at the right location.
+ gfx::Point mouse_offset_;
+
+ // A hint to use when positioning new windows created by detaching Tabs. This
+ // is the distance of the mouse from the top left of the dragged tab as if it
+ // were the distance of the mouse from the top left of the first tab in the
+ // attached TabStrip from the top left of the window.
+ gfx::Point window_create_point_;
+
+ // The bounds of the browser window before the last Tab was detached. When
+ // the last Tab is detached, rather than destroying the frame (which would
+ // abort the drag session), the frame is moved off-screen. If the drag is
+ // aborted (e.g. by the user pressing Esc, or capture being lost), the Tab is
+ // attached to the hidden frame and the frame moved back to these bounds.
+ gfx::Rect restore_bounds_;
+
+ // The last view that had focus in the window containing |source_tab_|. This
+ // is saved so that focus can be restored properly when a drag begins and
+ // ends within this same window.
+ ChromeViews::View* old_focused_view_;
+
+ bool in_destructor_;
+
+ DISALLOW_EVIL_CONSTRUCTORS(DraggedTabController);
+};
+
+#endif // CHROME_BROWSER_TABS_DRAGGED_TAB_CONTROLLER_H__
diff --git a/chrome/browser/tabs/dragged_tab_view.cc b/chrome/browser/tabs/dragged_tab_view.cc
new file mode 100644
index 0000000..d452290
--- /dev/null
+++ b/chrome/browser/tabs/dragged_tab_view.cc
@@ -0,0 +1,267 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "chrome/browser/tabs/dragged_tab_view.h"
+
+#include "chrome/common/gfx/chrome_canvas.h"
+#include "chrome/browser/tab_contents.h"
+#include "chrome/browser/tabs/hwnd_photobooth.h"
+#include "chrome/browser/tabs/tab_renderer.h"
+#include "chrome/browser/tabs/tab_strip_model.h"
+#include "chrome/views/hwnd_view_container.h"
+#include "skia/include/SkShader.h"
+
+const int kTransparentAlpha = 200;
+const int kOpaqueAlpha = 255;
+const int kDragFrameBorderSize = 2;
+const int kTwiceDragFrameBorderSize = 2 * kDragFrameBorderSize;
+const float kScalingFactor = 0.5;
+const int kAnimateToBoundsDurationMs = 150;
+static const SkColor kDraggedTabBorderColor = SkColorSetRGB(103, 129, 162);
+
+////////////////////////////////////////////////////////////////////////////////
+// DraggedTabView, public:
+
+DraggedTabView::DraggedTabView(TabContents* datasource,
+ const gfx::Point& mouse_tab_offset,
+ const gfx::Size& contents_size)
+ : container_(NULL),
+ renderer_(new TabRenderer),
+ attached_(false),
+ mouse_tab_offset_(mouse_tab_offset),
+ attached_tab_size_(TabRenderer::GetMinimumSelectedSize()),
+ photobooth_(NULL),
+ contents_size_(contents_size),
+ close_animation_(this) {
+ SetParentOwned(false);
+
+ renderer_->UpdateData(datasource);
+
+ container_ = new ChromeViews::HWNDViewContainer;
+ container_->set_window_style(WS_POPUP);
+ container_->set_window_ex_style(
+ WS_EX_LAYERED | WS_EX_TOPMOST | WS_EX_TOOLWINDOW);
+ container_->set_can_update_layered_window(false);
+ container_->Init(NULL, gfx::Rect(0, 0, 0, 0), this, false);
+}
+
+DraggedTabView::~DraggedTabView() {
+ if (close_animation_.IsAnimating())
+ close_animation_.Stop();
+ GetParent()->RemoveChildView(this);
+ container_->Close();
+}
+
+void DraggedTabView::MoveTo(const gfx::Point& screen_point) {
+ if (!container_->IsVisible())
+ container_->ShowWindow(SW_SHOWNOACTIVATE);
+
+ int x;
+ if (UILayoutIsRightToLeft() && !attached_) {
+ // On RTL locales, a dragged tab (when it is not attached to a tab strip)
+ // is rendered using a right-to-left orientation so we should calculate the
+ // window position differently.
+ CSize ps;
+ GetPreferredSize(&ps);
+ x = screen_point.x() - ScaleValue(ps.cx) + mouse_tab_offset_.x() +
+ ScaleValue(
+ renderer_->MirroredXCoordinateInsideView(mouse_tab_offset_.x()));
+ } else {
+ x = screen_point.x() + mouse_tab_offset_.x() -
+ ScaleValue(mouse_tab_offset_.x());
+ }
+ int y = screen_point.y() + mouse_tab_offset_.y() -
+ ScaleValue(mouse_tab_offset_.y());
+
+ container_->SetWindowPos(NULL, x, y, 0, 0, SWP_NOSIZE | SWP_NOACTIVATE);
+}
+
+void DraggedTabView::Attach(int selected_width) {
+ attached_ = true;
+ photobooth_ = NULL;
+ attached_tab_size_.set_width(selected_width);
+ container_->SetLayeredAlpha(kOpaqueAlpha);
+ ResizeContainer();
+ Update();
+}
+
+void DraggedTabView::Detach(HWNDPhotobooth* photobooth) {
+ attached_ = false;
+ photobooth_ = photobooth;
+ container_->SetLayeredAlpha(kTransparentAlpha);
+ ResizeContainer();
+ Update();
+}
+
+void DraggedTabView::Update() {
+ container_->set_can_update_layered_window(true);
+ SchedulePaint();
+ container_->PaintNow(CRect());
+ container_->set_can_update_layered_window(false);
+}
+
+void DraggedTabView::AnimateToBounds(const gfx::Rect& bounds,
+ Callback0::Type* callback) {
+ animation_callback_.reset(callback);
+
+ RECT wr;
+ GetWindowRect(GetViewContainer()->GetHWND(), &wr);
+ animation_start_bounds_ = wr;
+ animation_end_bounds_ = bounds;
+
+ close_animation_.SetSlideDuration(kAnimateToBoundsDurationMs);
+ close_animation_.SetTweenType(SlideAnimation::EASE_OUT);
+ if (!close_animation_.IsShowing()) {
+ close_animation_.Reset();
+ close_animation_.Show();
+ }
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// DraggedTabView, AnimationDelegate implementation:
+
+void DraggedTabView::AnimationProgressed(const Animation* animation) {
+ int delta_x = (animation_end_bounds_.x() - animation_start_bounds_.x());
+ int x = animation_start_bounds_.x() +
+ static_cast<int>(delta_x * animation->GetCurrentValue());
+ int y = animation_end_bounds_.y();
+ container_->SetWindowPos(NULL, x, y, 0, 0, SWP_NOSIZE | SWP_NOACTIVATE);
+}
+
+void DraggedTabView::AnimationEnded(const Animation* animation) {
+ animation_callback_->Run();
+}
+
+void DraggedTabView::AnimationCanceled(const Animation* animation) {
+ AnimationEnded(animation);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// DraggedTabView, ChromeViews::View overrides:
+
+void DraggedTabView::Paint(ChromeCanvas* canvas) {
+ if (attached_) {
+ PaintAttachedTab(canvas);
+ } else {
+ PaintDetachedView(canvas);
+ }
+}
+
+void DraggedTabView::Layout() {
+ CSize ps;
+ GetPreferredSize(&ps);
+ if (attached_) {
+ renderer_->SetBounds(CRect(0, 0, ps.cx, ps.cy));
+ } else {
+ int left = 0;
+ if (UILayoutIsRightToLeft())
+ left = ps.cx - attached_tab_size_.width();
+ renderer_->SetBounds(CRect(left, 0, left + attached_tab_size_.width(),
+ attached_tab_size_.height()));
+ }
+}
+
+void DraggedTabView::GetPreferredSize(CSize* out) {
+ DCHECK(out);
+ if (attached_) {
+ *out = attached_tab_size_.ToSIZE();
+ } else {
+ int width = std::max(attached_tab_size_.width(), contents_size_.width()) +
+ kTwiceDragFrameBorderSize;
+ int height = attached_tab_size_.height() + kDragFrameBorderSize +
+ contents_size_.height();
+ *out = CSize(width, height);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// DraggedTabView, private:
+
+void DraggedTabView::PaintAttachedTab(ChromeCanvas* canvas) {
+ renderer_->ProcessPaint(canvas);
+}
+
+void DraggedTabView::PaintDetachedView(ChromeCanvas* canvas) {
+ CSize ps;
+ GetPreferredSize(&ps);
+ ChromeCanvas scale_canvas(ps.cx, ps.cy, false);
+ SkBitmap& bitmap_device = const_cast<SkBitmap&>(
+ scale_canvas.getTopPlatformDevice().accessBitmap(true));
+ bitmap_device.eraseARGB(0, 0, 0, 0);
+
+ scale_canvas.FillRectInt(kDraggedTabBorderColor, 0,
+ attached_tab_size_.height() - kDragFrameBorderSize,
+ ps.cx, ps.cy - attached_tab_size_.height());
+ int image_x = kDragFrameBorderSize;
+ int image_y = attached_tab_size_.height();
+ int image_w = ps.cx - kTwiceDragFrameBorderSize;
+ int image_h =
+ ps.cy - kTwiceDragFrameBorderSize - attached_tab_size_.height();
+ scale_canvas.FillRectInt(SK_ColorBLACK, image_x, image_y, image_w, image_h);
+ photobooth_->PaintScreenshotIntoCanvas(
+ &scale_canvas,
+ gfx::Rect(image_x, image_y, image_w, image_h));
+ renderer_->ProcessPaint(&scale_canvas);
+
+ SkIRect subset;
+ subset.set(0, 0, ps.cx, ps.cy);
+ SkBitmap mipmap = scale_canvas.ExtractBitmap();
+ mipmap.buildMipMap(true);
+
+ SkShader* bitmap_shader =
+ SkShader::CreateBitmapShader(mipmap, SkShader::kClamp_TileMode,
+ SkShader::kClamp_TileMode);
+
+ SkMatrix shader_scale;
+ shader_scale.setScale(kScalingFactor, kScalingFactor);
+ bitmap_shader->setLocalMatrix(shader_scale);
+
+ SkPaint paint;
+ paint.setShader(bitmap_shader);
+ paint.setAntiAlias(true);
+ bitmap_shader->unref();
+
+ SkRect rc;
+ rc.fLeft = 0;
+ rc.fTop = 0;
+ rc.fRight = SkIntToScalar(ps.cx);
+ rc.fBottom = SkIntToScalar(ps.cy);
+ canvas->drawRect(rc, paint);
+}
+
+void DraggedTabView::ResizeContainer() {
+ CSize ps;
+ GetPreferredSize(&ps);
+ SetWindowPos(container_->GetHWND(), HWND_TOPMOST, 0, 0, ScaleValue(ps.cx),
+ ScaleValue(ps.cy), SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
+}
+
+int DraggedTabView::ScaleValue(int value) {
+ return attached_ ? value : static_cast<int>(value * kScalingFactor);
+}
diff --git a/chrome/browser/tabs/dragged_tab_view.h b/chrome/browser/tabs/dragged_tab_view.h
new file mode 100644
index 0000000..da0057b
--- /dev/null
+++ b/chrome/browser/tabs/dragged_tab_view.h
@@ -0,0 +1,143 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef CHROME_BROWSER_TABS_DRAGGED_TAB_VIEW_H_
+#define CHROME_BROWSER_TABS_DRAGGED_TAB_VIEW_H_
+
+#include "base/gfx/point.h"
+#include "base/gfx/size.h"
+#include "base/task.h"
+#include "chrome/common/slide_animation.h"
+#include "chrome/views/view.h"
+#include "skia/include/SkBitmap.h"
+
+namespace ChromeViews {
+class HWNDViewContainer;
+}
+namespace gfx {
+class Point;
+}
+class HWNDPhotobooth;
+class Tab;
+class TabContents;
+class TabRenderer;
+
+class DraggedTabView : public ChromeViews::View,
+ public AnimationDelegate {
+ public:
+ DraggedTabView(TabContents* datasource,
+ const gfx::Point& mouse_tab_offset,
+ const gfx::Size& contents_size);
+ virtual ~DraggedTabView();
+
+ // Moves the DraggedTabView to the appropriate location given the mouse
+ // pointer at |screen_point|.
+ void MoveTo(const gfx::Point& screen_point);
+
+ // Notifies the DraggedTabView that it has become attached to a TabStrip.
+ void Attach(int selected_width);
+
+ // Notifies the DraggedTabView that it has been detached from a TabStrip.
+ void Detach(HWNDPhotobooth* photobooth);
+
+ // Notifies the DraggedTabView that it should update itself.
+ void Update();
+
+ // Animates the DraggedTabView to the specified bounds, then calls back to
+ // |callback|.
+ void AnimateToBounds(const gfx::Rect& bounds, Callback0::Type* callback);
+
+ // Returns the size of the DraggedTabView. Used when attaching to a TabStrip
+ // to determine where to place the Tab in the attached TabStrip.
+ gfx::Size attached_tab_size() const { return attached_tab_size_; }
+
+ private:
+ // Overridden from AnimationDelegate:
+ virtual void AnimationProgressed(const Animation* animation);
+ virtual void AnimationEnded(const Animation* animation);
+ virtual void AnimationCanceled(const Animation* animation);
+
+ // Overridden from ChromeViews::View:
+ virtual void Paint(ChromeCanvas* canvas);
+ virtual void Layout();
+ virtual void GetPreferredSize(CSize* out);
+
+ // Paint the view, when it's attached to a TabStrip.
+ void PaintAttachedTab(ChromeCanvas* canvas);
+
+ // Paint the view, when it's not attached to any TabStrip.
+ void PaintDetachedView(ChromeCanvas* canvas);
+
+ // Resizes the container to fit the content for the current attachment mode.
+ void ResizeContainer();
+
+ // Utility for scaling a size by the current scaling factor.
+ int ScaleValue(int value);
+
+ // The window that contains the DraggedTabView.
+ ChromeViews::HWNDViewContainer* container_;
+
+ // The renderer that paints the Tab shape.
+ scoped_ptr<TabRenderer> renderer_;
+
+ // True if the view is currently attached to a TabStrip. Controls rendering
+ // and sizing modes.
+ bool attached_;
+
+ // The unscaled offset of the mouse from the top left of the dragged Tab.
+ // This is used to maintain an appropriate offset for the mouse pointer when
+ // dragging scaled and unscaled representations, and also to calculate the
+ // position of detached windows.
+ gfx::Point mouse_tab_offset_;
+
+ // The desired width of the TabRenderer when the DraggedTabView is attached
+ // to a TabStrip.
+ gfx::Size attached_tab_size_;
+
+ // A handle to the DIB containing the current screenshot of the TabContents
+ // we are dragging.
+ HWNDPhotobooth* photobooth_;
+
+ // The dimensions of the TabContents being dragged.
+ gfx::Size contents_size_;
+
+ // The animation used to slide the attached view to its final location.
+ SlideAnimation close_animation_;
+
+ // A callback notified when the animation is complete.
+ scoped_ptr<Callback0::Type> animation_callback_;
+
+ // The start and end bounds of the animation sequence.
+ gfx::Rect animation_start_bounds_;
+ gfx::Rect animation_end_bounds_;
+
+ DISALLOW_EVIL_CONSTRUCTORS(DraggedTabView);
+};
+
+#endif // CHROME_BROWSER_TABS_DRAGGED_TAB_VIEW_H_
diff --git a/chrome/browser/tabs/hwnd_photobooth.cc b/chrome/browser/tabs/hwnd_photobooth.cc
new file mode 100644
index 0000000..d8a9348
--- /dev/null
+++ b/chrome/browser/tabs/hwnd_photobooth.cc
@@ -0,0 +1,184 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "base/gfx/point.h"
+#include "chrome/browser/tab_contents.h"
+#include "chrome/browser/tabs/hwnd_photobooth.h"
+#include "chrome/common/gfx/chrome_canvas.h"
+#include "chrome/views/hwnd_view_container.h"
+#include "skia/include/SkBitmap.h"
+
+namespace {
+
+static BOOL CALLBACK MonitorEnumProc(HMONITOR monitor, HDC monitor_dc,
+ RECT* monitor_rect, LPARAM data) {
+ gfx::Point* point = reinterpret_cast<gfx::Point*>(data);
+ if (monitor_rect->right > point->x() && monitor_rect->bottom > point->y()) {
+ point->set_x(monitor_rect->right);
+ point->set_y(monitor_rect->bottom);
+ }
+ return TRUE;
+}
+
+gfx::Point GetCaptureWindowPosition() {
+ // Since the capture window must be visible to be painted, it must be opened
+ // off screen to avoid flashing. But if it is opened completely off-screen
+ // (e.g. at 0xFFFFx0xFFFF) then on Windows Vista it will not paint even if it
+ // _is_ visible. So we need to find the right/bottommost monitor, and
+ // position it so that 1x1 pixel is on-screen on that monitor which is enough
+ // to convince Vista to paint it. Don't ask why this is so - this appears to
+ // be a regression over XP.
+ gfx::Point point(0, 0);
+ EnumDisplayMonitors(NULL, NULL, &MonitorEnumProc,
+ reinterpret_cast<LPARAM>(&point));
+ return gfx::Point(point.x() - 1, point.y() - 1);
+}
+
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// HWNDPhotobooth, public:
+
+HWNDPhotobooth::HWNDPhotobooth(HWND initial_hwnd)
+ : capture_window_(NULL),
+ current_hwnd_(initial_hwnd) {
+ DCHECK(IsWindow(current_hwnd_));
+ CreateCaptureWindow(initial_hwnd);
+}
+
+HWNDPhotobooth::~HWNDPhotobooth() {
+ // Detach the attached HWND. The creator of the photo-booth is responsible
+ // for destroying it.
+ ReplaceHWND(NULL);
+ capture_window_->Close();
+}
+
+void HWNDPhotobooth::ReplaceHWND(HWND new_hwnd) {
+ if (IsWindow(current_hwnd_) &&
+ GetParent(current_hwnd_) == capture_window_->GetHWND()) {
+ // We need to hide the window too, so it doesn't show up in the TaskBar or
+ // be parented to the desktop.
+ ShowWindow(current_hwnd_, SW_HIDE);
+ SetParent(current_hwnd_, NULL);
+ }
+ current_hwnd_ = new_hwnd;
+
+ if (IsWindow(new_hwnd)) {
+ // Insert the TabContents into the capture window.
+ SetParent(current_hwnd_, capture_window_->GetHWND());
+
+ // Show the window (it may not be visible). This is the only safe way of
+ // doing this. ShowWindow does not work.
+ SetWindowPos(current_hwnd_, NULL, 0, 0, 0, 0,
+ SWP_DEFERERASE | SWP_NOACTIVATE | SWP_NOCOPYBITS |
+ SWP_NOOWNERZORDER | SWP_NOSENDCHANGING | SWP_NOZORDER |
+ SWP_SHOWWINDOW | SWP_NOSIZE);
+ }
+}
+
+void HWNDPhotobooth::PaintScreenshotIntoCanvas(
+ ChromeCanvas* canvas,
+ const gfx::Rect& target_bounds) {
+ // Our contained window may have been re-parented. Make sure it belongs to
+ // us until someone calls ReplaceHWND(NULL).
+ if (IsWindow(current_hwnd_) &&
+ GetParent(current_hwnd_) != capture_window_->GetHWND()) {
+ ReplaceHWND(current_hwnd_);
+ }
+
+ // We compel the contained HWND to paint now, synchronously. We do this to
+ // populate the device context with valid and current data.
+ RedrawWindow(current_hwnd_, NULL, NULL, RDW_INVALIDATE | RDW_UPDATENOW);
+
+ // Transfer the contents of the layered capture window to the screen-shot
+ // canvas' DIB.
+ HDC target_dc = canvas->beginPlatformPaint();
+ HDC source_dc = GetDC(current_hwnd_);
+ RECT window_rect = {0};
+ GetWindowRect(current_hwnd_, &window_rect);
+ BitBlt(target_dc, target_bounds.x(), target_bounds.y(),
+ target_bounds.width(), target_bounds.height(), source_dc, 0, 0,
+ SRCCOPY);
+ // Windows screws up the alpha channel on all text it draws, and so we need
+ // to call makeOpaque _after_ the blit to correct for this.
+ canvas->getTopPlatformDevice().makeOpaque(target_bounds.x(),
+ target_bounds.y(),
+ target_bounds.width(),
+ target_bounds.height());
+ ReleaseDC(current_hwnd_, source_dc);
+ canvas->endPlatformPaint();
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// HWNDPhotobooth, private:
+
+void HWNDPhotobooth::CreateCaptureWindow(HWND initial_hwnd) {
+ // Snapshotting a HWND is tricky - if the HWND is clipped (e.g. positioned
+ // partially off-screen) then just blitting from the HWND' DC to the capture
+ // bitmap would be incorrect, since the capture bitmap would show only the
+ // visible area of the HWND.
+ //
+ // The approach turns out to be to create a second layered window in
+ // hyperspace the to act as a "photo booth." The window is created with the
+ // size of the unclipped HWND, and we attach the HWND as a child, refresh the
+ // HWND' by calling |Paint| on it, and then blitting from the HWND's DC to
+ // the capture bitmap. This results in the entire unclipped HWND display
+ // bitmap being captured.
+ //
+ // The capture window must be layered so that Windows generates a backing
+ // store for it, so that blitting from a child window's DC produces data. If
+ // the window is not layered, because it is off-screen Windows does not
+ // retain its contents and blitting results in blank data. The capture window
+ // is a "basic" (1 level of alpha) layered window because that is the mode
+ // that supports having child windows (variable alpha layered windows do not
+ // support child HWNDs).
+ //
+ // This function sets up the off-screen capture window, and attaches the
+ // associated HWND to it. Note that the details are important here, see below
+ // for further comments.
+ //
+ CRect contents_rect;
+ GetClientRect(initial_hwnd, &contents_rect);
+ gfx::Point window_position = GetCaptureWindowPosition();
+ gfx::Rect capture_bounds(window_position.x(), window_position.y(),
+ contents_rect.Width(), contents_rect.Height());
+ capture_window_ = new ChromeViews::HWNDViewContainer;
+ capture_window_->set_window_style(WS_POPUP);
+ // WS_EX_TOOLWINDOW ensures the capture window doesn't produce a Taskbar
+ // button.
+ capture_window_->set_window_ex_style(WS_EX_LAYERED | WS_EX_TOOLWINDOW);
+ capture_window_->Init(NULL, capture_bounds, NULL, false);
+ // If the capture window isn't visible, blitting from the TabContents'
+ // HWND's DC to the capture bitmap produces blankness.
+ capture_window_->ShowWindow(SW_SHOWNOACTIVATE);
+ SetLayeredWindowAttributes(
+ capture_window_->GetHWND(), RGB(0xFF, 0xFF, 0xFF), 0xFF, LWA_ALPHA);
+
+ ReplaceHWND(initial_hwnd);
+} \ No newline at end of file
diff --git a/chrome/browser/tabs/hwnd_photobooth.h b/chrome/browser/tabs/hwnd_photobooth.h
new file mode 100644
index 0000000..2ddd053
--- /dev/null
+++ b/chrome/browser/tabs/hwnd_photobooth.h
@@ -0,0 +1,86 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef CHROME_BROWSER_HWND_PHOTOBOOTH_H__
+#define CHROME_BROWSER_HWND_PHOTOBOOTH_H__
+
+#include "base/basictypes.h"
+#include "base/gfx/rect.h"
+
+class ChromeCanvas;
+namespace ChromeViews {
+class HWNDViewContainer;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// HWNDPhotobooth
+//
+// An object that a HWND "steps into" to have its picture taken. This is used
+// to generate a full size screen shot of the contents of a HWND including
+// any child windows.
+//
+// Implementation note: This causes the HWND to be re-parented to a mostly
+// off-screen layered window.
+//
+class HWNDPhotobooth {
+ public:
+ // Creates the photo booth. Constructs a nearly off-screen window, parents
+ // the HWND, then shows it. The caller is responsible for destroying this
+ // window, since the photo-booth will detach it before it is destroyed.
+ // |canvas| is a canvas to paint the contents into, and dest_bounds is the
+ // target area in |canvas| to which painted contents will be clipped.
+ explicit HWNDPhotobooth(HWND initial_hwnd);
+
+ // Destroys the photo booth window.
+ virtual ~HWNDPhotobooth();
+
+ // Replaces the HWND in the photo booth with the specified one. The caller is
+ // responsible for destroying this HWND since it will be detached from the
+ // capture window before the capture window is destroyed.
+ void ReplaceHWND(HWND new_hwnd);
+
+ // Paints the current display image of the window into |canvas|, clipped to
+ // |target_bounds|.
+ void PaintScreenshotIntoCanvas(ChromeCanvas* canvas,
+ const gfx::Rect& target_bounds);
+
+ private:
+ // Creates a mostly off-screen window to contain the HWND to be captured.
+ void CreateCaptureWindow(HWND initial_hwnd);
+
+ // The nearly off-screen photo-booth layered window used to hold the HWND.
+ ChromeViews::HWNDViewContainer* capture_window_;
+
+ // The current HWND being captured.
+ HWND current_hwnd_;
+
+ DISALLOW_EVIL_CONSTRUCTORS(HWNDPhotobooth);
+};
+
+#endif // #ifndef CHROME_BROWSER_HWND_PHOTOBOOTH_H__
diff --git a/chrome/browser/tabs/tab.cc b/chrome/browser/tabs/tab.cc
new file mode 100644
index 0000000..df02a15
--- /dev/null
+++ b/chrome/browser/tabs/tab.cc
@@ -0,0 +1,184 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "chrome/browser/tabs/tab.h"
+
+#include "base/gfx/size.h"
+#include "chrome/browser/tab_contents.h"
+#include "chrome/browser/tabs/tab_strip.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/user_metrics.h"
+#include "chrome/common/l10n_util.h"
+#include "chrome/common/resource_bundle.h"
+#include "chrome/views/tooltip_manager.h"
+#include "generated_resources.h"
+
+const std::string Tab::kTabClassName = "browser/tabs/Tab";
+
+///////////////////////////////////////////////////////////////////////////////
+// Tab, public:
+
+Tab::Tab(TabDelegate* delegate)
+ : TabRenderer(),
+ delegate_(delegate),
+ closing_(false) {
+ close_button()->SetListener(this, 0);
+ close_button()->SetAccessibleName(l10n_util::GetString(IDS_ACCNAME_CLOSE));
+ close_button()->SetAnimationDuration(0);
+}
+
+Tab::~Tab() {
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Tab, TabRenderer overrides:
+
+bool Tab::IsSelected() const {
+ return delegate_->IsTabSelected(this);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Tab, ChromeViews::View overrides:
+
+bool Tab::OnMousePressed(const ChromeViews::MouseEvent& event) {
+ if (event.IsOnlyLeftMouseButton()) {
+ // Store whether or not we were selected just now... we only want to be
+ // able to drag foreground tabs, so we don't start dragging the tab if
+ // it was in the background.
+ bool just_selected = !IsSelected();
+ if (just_selected)
+ delegate_->SelectTab(this);
+ delegate_->MaybeStartDrag(this, event);
+ }
+ return true;
+}
+
+bool Tab::OnMouseDragged(const ChromeViews::MouseEvent& event) {
+ delegate_->ContinueDrag(event);
+ return true;
+}
+
+void Tab::OnMouseReleased(const ChromeViews::MouseEvent& event,
+ bool canceled) {
+ // Notify the drag helper that we're done with any potential drag operations.
+ // Clean up the drag helper, which is re-created on the next mouse press.
+ delegate_->EndDrag(canceled);
+ if (event.IsOnlyRightMouseButton()) {
+ CPoint screen_point = event.GetLocation();
+ ConvertPointToScreen(this, &screen_point);
+ RunContextMenuAt(gfx::Point(screen_point));
+ } else if (event.IsMiddleMouseButton()) {
+ delegate_->CloseTab(this);
+ }
+}
+
+bool Tab::GetTooltipText(int x, int y, std::wstring* tooltip) {
+ std::wstring title = GetTitle();
+ if (!title.empty()) {
+ // Only show the tooltip if the title is truncated.
+ ChromeFont font;
+ if (font.GetStringWidth(title) > title_bounds().width()) {
+ *tooltip = title;
+ return true;
+ }
+ }
+ return false;
+}
+
+bool Tab::GetTooltipTextOrigin(int x, int y, CPoint* origin) {
+ ChromeFont font;
+ origin->x = title_bounds().x() + 10;
+ origin->y = -ChromeViews::TooltipManager::GetTooltipHeight() - 4;
+ return true;
+}
+
+bool Tab::GetAccessibleRole(VARIANT* role) {
+ DCHECK(role);
+
+ role->vt = VT_I4;
+ role->lVal = ROLE_SYSTEM_PAGETAB;
+ return true;
+}
+
+bool Tab::GetAccessibleName(std::wstring* name) {
+ *name = GetTitle();
+ return !name->empty();
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Tab, ChromeViews::Menu::Delegate implementation:
+
+bool Tab::IsCommandEnabled(int id) const {
+ return delegate_->IsCommandEnabledForTab(
+ static_cast<TabStripModel::ContextMenuCommand>(id), this);
+}
+
+void Tab::ExecuteCommand(int id) {
+ delegate_->ExecuteCommandForTab(
+ static_cast<TabStripModel::ContextMenuCommand>(id), this);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// ChromeViews::BaseButton::ButtonListener implementation:
+
+void Tab::ButtonPressed(ChromeViews::BaseButton* sender) {
+ if (sender == close_button())
+ delegate_->CloseTab(this);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Tab, private
+
+void Tab::RunContextMenuAt(const gfx::Point& screen_point) {
+ Menu menu(this, Menu::TOPLEFT, GetViewContainer()->GetHWND());
+ menu.AppendMenuItem(TabStripModel::CommandNewTab,
+ l10n_util::GetString(IDS_TAB_CXMENU_NEWTAB),
+ Menu::NORMAL);
+ menu.AppendSeparator();
+ menu.AppendMenuItem(TabStripModel::CommandReload,
+ l10n_util::GetString(IDS_TAB_CXMENU_RELOAD),
+ Menu::NORMAL);
+ menu.AppendMenuItem(TabStripModel::CommandDuplicate,
+ l10n_util::GetString(IDS_TAB_CXMENU_DUPLICATE),
+ Menu::NORMAL);
+ menu.AppendSeparator();
+ menu.AppendMenuItem(TabStripModel::CommandCloseTab,
+ l10n_util::GetString(IDS_TAB_CXMENU_CLOSETAB),
+ Menu::NORMAL);
+ menu.AppendMenuItem(TabStripModel::CommandCloseOtherTabs,
+ l10n_util::GetString(IDS_TAB_CXMENU_CLOSEOTHERTABS),
+ Menu::NORMAL);
+ menu.AppendMenuItem(TabStripModel::CommandCloseTabsToRight,
+ l10n_util::GetString(IDS_TAB_CXMENU_CLOSETABSTORIGHT),
+ Menu::NORMAL);
+ menu.AppendMenuItem(TabStripModel::CommandCloseTabsOpenedBy,
+ l10n_util::GetString(IDS_TAB_CXMENU_CLOSETABSOPENEDBY),
+ Menu::NORMAL);
+ menu.RunMenuAt(screen_point.x(), screen_point.y());
+}
diff --git a/chrome/browser/tabs/tab.h b/chrome/browser/tabs/tab.h
new file mode 100644
index 0000000..a7999d3
--- /dev/null
+++ b/chrome/browser/tabs/tab.h
@@ -0,0 +1,134 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef CHROME_BROWSER_TABS_TAB_H__
+#define CHROME_BROWSER_TABS_TAB_H__
+
+#include "base/gfx/point.h"
+#include "chrome/browser/tabs/tab_renderer.h"
+#include "chrome/browser/tabs/tab_strip_model.h"
+#include "chrome/views/button.h"
+#include "chrome/views/menu.h"
+
+namespace gfx {
+class Point;
+}
+class TabContents;
+class Profile;
+
+///////////////////////////////////////////////////////////////////////////////
+//
+// Tab
+//
+// A subclass of TabRenderer that represents an individual Tab in a TabStrip.
+//
+///////////////////////////////////////////////////////////////////////////////
+class Tab : public TabRenderer,
+ public Menu::Delegate,
+ public ChromeViews::BaseButton::ButtonListener {
+ public:
+ static const std::string kTabClassName;
+
+ // An interface implemented by an object that can help this Tab complete
+ // various actions. The index parameter is the index of this Tab in the
+ // TabRenderer::Model.
+ class TabDelegate {
+ public:
+ // Returns true if the specified Tab is selected.
+ virtual bool IsTabSelected(const Tab* tab) const = 0;
+
+ // Selects the specified Tab.
+ virtual void SelectTab(Tab* tab) = 0;
+
+ // Closes the specified Tab.
+ virtual void CloseTab(Tab* tab) = 0;
+
+ // Returns true if the specified command is enabled for the specified Tab.
+ virtual bool IsCommandEnabledForTab(
+ TabStripModel::ContextMenuCommand command_id, const Tab* tab) const = 0;
+
+ // Executes the specified command for the specified Tab.
+ virtual void ExecuteCommandForTab(
+ TabStripModel::ContextMenuCommand command_id, Tab* tab) = 0;
+
+ // Potentially starts a drag for the specified Tab.
+ virtual void MaybeStartDrag(Tab* tab,
+ const ChromeViews::MouseEvent& event) = 0;
+
+ // Continues dragging a Tab.
+ virtual void ContinueDrag(const ChromeViews::MouseEvent& event) = 0;
+
+ // Ends dragging a Tab. |canceled| is true if the drag was aborted in a way
+ // other than the user releasing the mouse.
+ virtual void EndDrag(bool canceled) = 0;
+ };
+
+ explicit Tab(TabDelegate* delegate);
+ virtual ~Tab();
+
+ // Used to set/check whether this Tab is being animated closed.
+ void set_closing(bool closing) { closing_ = closing; }
+ bool closing() const { return closing_; }
+
+ // TabRenderer overrides:
+ virtual bool IsSelected() const;
+
+ private:
+ // ChromeViews::View overrides:
+ virtual bool OnMousePressed(const ChromeViews::MouseEvent& event);
+ virtual bool OnMouseDragged(const ChromeViews::MouseEvent& event);
+ virtual void OnMouseReleased(const ChromeViews::MouseEvent& event,
+ bool canceled);
+ virtual bool GetTooltipText(int x, int y, std::wstring* tooltip);
+ virtual bool GetTooltipTextOrigin(int x, int y, CPoint* origin);
+ virtual std::string GetClassName() const { return kTabClassName; }
+ virtual bool GetAccessibleRole(VARIANT* role);
+ virtual bool GetAccessibleName(std::wstring* name);
+
+ // ChromeViews::Menu::Delegate overrides:
+ virtual bool IsCommandEnabled(int id) const;
+ virtual void ExecuteCommand(int id);
+
+ // ChromeViews::BaseButton::ButtonListener overrides:
+ virtual void ButtonPressed(ChromeViews::BaseButton* sender);
+
+ // Run a context menu for this Tab at the specified screen point.
+ void RunContextMenuAt(const gfx::Point& screen_point);
+
+ // An instance of a delegate object that can perform various actions based on
+ // user gestures.
+ TabDelegate* delegate_;
+
+ // True if the tab is being animated closed.
+ bool closing_;
+
+ DISALLOW_EVIL_CONSTRUCTORS(Tab);
+};
+
+#endif CHROME_BROWSER_TABS_TAB_H__
diff --git a/chrome/browser/tabs/tab_renderer.cc b/chrome/browser/tabs/tab_renderer.cc
new file mode 100644
index 0000000..5bfbcf0
--- /dev/null
+++ b/chrome/browser/tabs/tab_renderer.cc
@@ -0,0 +1,691 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "chrome/browser/tabs/tab_renderer.h"
+
+#include "base/gfx/image_operations.h"
+#include "chrome/app/theme/theme_resources.h"
+#include "chrome/browser/tabs/tab_strip_model.h"
+#include "chrome/browser/tab_contents.h"
+#include "chrome/common/gfx/chrome_canvas.h"
+#include "chrome/common/gfx/chrome_font.h"
+#include "chrome/common/resource_bundle.h"
+#include "chrome/common/win_util.h"
+#include "generated_resources.h"
+
+static const int kLeftPadding = 16;
+static const int kTopPadding = 6;
+static const int kRightPadding = 15;
+static const int kBottomPadding = 5;
+static const int kFavIconTitleSpacing = 4;
+static const int kTitleCloseButtonSpacing = 5;
+static const int kStandardTitleWidth = 175;
+static const int kCloseButtonVertFuzz = 0;
+static const int kCloseButtonHorzFuzz = 5;
+static const int kFaviconSize = 16;
+static const int kSelectedTitleColor = SK_ColorBLACK;
+static const int kUnselectedTitleColor = SkColorSetRGB(64, 64, 64);
+
+// How long the hover state takes.
+static const int kHoverDurationMs = 90;
+
+// How opaque to make the hover state (out of 1).
+static const double kHoverOpacity = 0.33;
+static const double kHoverOpacityVista = 0.7;
+
+// TODO(beng): (Cleanup) This stuff should move onto the class.
+static ChromeFont title_font;
+static int title_font_height = 0;
+static SkBitmap* close_button_n = NULL;
+static SkBitmap* close_button_h = NULL;
+static SkBitmap* close_button_p = NULL;
+static int close_button_height = 0;
+static int close_button_width = 0;
+static SkBitmap* tab_active_l = NULL;
+static SkBitmap* tab_active_c = NULL;
+static SkBitmap* tab_active_r = NULL;
+static int tab_active_l_width = 0;
+static int tab_active_r_width = 0;
+static SkBitmap* tab_inactive_l = NULL;
+static SkBitmap* tab_inactive_c = NULL;
+static SkBitmap* tab_inactive_r = NULL;
+static SkBitmap* tab_inactive_otr_l = NULL;
+static SkBitmap* tab_inactive_otr_c = NULL;
+static SkBitmap* tab_inactive_otr_r = NULL;
+static SkBitmap* tab_hover_l = NULL;
+static SkBitmap* tab_hover_c = NULL;
+static SkBitmap* tab_hover_r = NULL;
+static int tab_inactive_l_width = 0;
+static int tab_inactive_r_width = 0;
+static SkBitmap* waiting_animation_frames = NULL;
+static SkBitmap* loading_animation_frames = NULL;
+static SkBitmap* crashed_fav_icon = NULL;
+static int loading_animation_frame_count = 0;
+static int waiting_animation_frame_count = 0;
+static int waiting_to_loading_frame_count_ratio = 0;
+static SkBitmap* download_icon = NULL;
+static int download_icon_width = 0;
+static int download_icon_height = 0;
+
+namespace {
+
+void InitResources() {
+ static bool initialized = false;
+ if (!initialized) {
+ ResourceBundle& rb = ResourceBundle::GetSharedInstance();
+ title_font = rb.GetFont(ResourceBundle::BaseFont);
+ title_font_height = title_font.height();
+
+ close_button_n = rb.GetBitmapNamed(IDR_TAB_CLOSE);
+ close_button_h = rb.GetBitmapNamed(IDR_TAB_CLOSE_H);
+ close_button_p = rb.GetBitmapNamed(IDR_TAB_CLOSE_P);
+ close_button_width = close_button_n->width();
+ close_button_height = close_button_n->height();
+
+ tab_active_l = rb.GetBitmapNamed(IDR_TAB_ACTIVE_LEFT);
+ tab_active_c = rb.GetBitmapNamed(IDR_TAB_ACTIVE_CENTER);
+ tab_active_r = rb.GetBitmapNamed(IDR_TAB_ACTIVE_RIGHT);
+ tab_active_l_width = tab_active_l->width();
+ tab_active_r_width = tab_active_r->width();
+
+ if (win_util::ShouldUseVistaFrame()) {
+ tab_inactive_l = rb.GetBitmapNamed(IDR_TAB_INACTIVE_LEFT_V);
+ tab_inactive_c = rb.GetBitmapNamed(IDR_TAB_INACTIVE_CENTER_V);
+ tab_inactive_r = rb.GetBitmapNamed(IDR_TAB_INACTIVE_RIGHT_V);
+
+ // Our Vista frame doesn't change background color to show OTR,
+ // so we continue to use the existing background tabs.
+ tab_inactive_otr_l = rb.GetBitmapNamed(IDR_TAB_INACTIVE_LEFT_V);
+ tab_inactive_otr_c = rb.GetBitmapNamed(IDR_TAB_INACTIVE_CENTER_V);
+ tab_inactive_otr_r = rb.GetBitmapNamed(IDR_TAB_INACTIVE_RIGHT_V);
+ } else {
+ tab_inactive_l = rb.GetBitmapNamed(IDR_TAB_INACTIVE_LEFT);
+ tab_inactive_c = rb.GetBitmapNamed(IDR_TAB_INACTIVE_CENTER);
+ tab_inactive_r = rb.GetBitmapNamed(IDR_TAB_INACTIVE_RIGHT);
+
+ tab_inactive_otr_l = rb.GetBitmapNamed(IDR_TAB_INACTIVE_LEFT_OTR);
+ tab_inactive_otr_c = rb.GetBitmapNamed(IDR_TAB_INACTIVE_CENTER_OTR);
+ tab_inactive_otr_r = rb.GetBitmapNamed(IDR_TAB_INACTIVE_RIGHT_OTR);
+ }
+
+ tab_hover_l = rb.GetBitmapNamed(IDR_TAB_HOVER_LEFT);
+ tab_hover_c = rb.GetBitmapNamed(IDR_TAB_HOVER_CENTER);
+ tab_hover_r = rb.GetBitmapNamed(IDR_TAB_HOVER_RIGHT);
+
+ tab_inactive_l_width = tab_inactive_l->width();
+ tab_inactive_r_width = tab_inactive_r->width();
+
+ // The loading animation image is a strip of states. Each state must be
+ // square, so the height must divide the width evenly.
+ loading_animation_frames = rb.GetBitmapNamed(IDR_THROBBER);
+ DCHECK(loading_animation_frames);
+ DCHECK(loading_animation_frames->width() %
+ loading_animation_frames->height() == 0);
+ loading_animation_frame_count =
+ loading_animation_frames->width() / loading_animation_frames->height();
+
+ waiting_animation_frames = rb.GetBitmapNamed(IDR_THROBBER_WAITING);
+ DCHECK(waiting_animation_frames);
+ DCHECK(waiting_animation_frames->width() %
+ waiting_animation_frames->height() == 0);
+ waiting_animation_frame_count =
+ waiting_animation_frames->width() / waiting_animation_frames->height();
+
+ waiting_to_loading_frame_count_ratio =
+ waiting_animation_frame_count / loading_animation_frame_count;
+
+ crashed_fav_icon = rb.GetBitmapNamed(IDR_SAD_FAVICON);
+
+ download_icon = rb.GetBitmapNamed(IDR_DOWNLOAD_ICON);
+ download_icon_width = download_icon->width();
+ download_icon_height = download_icon->height();
+
+ initialized = true;
+ }
+}
+
+int GetContentHeight() {
+ // The height of the content of the Tab is the largest of the favicon,
+ // the title text and the close button graphic.
+ int content_height = std::max(kFaviconSize, title_font_height);
+ return std::max(content_height, close_button_height);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// TabCloseButton
+//
+// This is a Button subclass that causes middle clicks to be forwarded to the
+// parent View by explicitly not handling them in OnMousePressed.
+class TabCloseButton : public ChromeViews::Button {
+ public:
+ TabCloseButton() : Button() {}
+ virtual ~TabCloseButton() {}
+
+ virtual bool OnMousePressed(const ChromeViews::MouseEvent& event) {
+ return !event.IsOnlyMiddleMouseButton();
+ }
+
+ // We need to let the parent know about mouse state so that it
+ // can highlight itself appropriately. Note that Exit events
+ // fire before Enter events, so this works.
+ virtual void OnMouseEntered(const ChromeViews::MouseEvent& event) {
+ BaseButton::OnMouseEntered(event);
+ GetParent()->OnMouseEntered(event);
+ }
+
+ virtual void OnMouseExited(const ChromeViews::MouseEvent& event) {
+ BaseButton::OnMouseExited(event);
+ GetParent()->OnMouseExited(event);
+ }
+
+ private:
+ DISALLOW_EVIL_CONSTRUCTORS(TabCloseButton);
+};
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+// FaviconCrashAnimation
+//
+// A custom animation subclass to manage the favicon crash animation.
+class TabRenderer::FavIconCrashAnimation : public Animation,
+ public AnimationDelegate {
+ public:
+ explicit FavIconCrashAnimation(TabRenderer* target)
+ : Animation(1000, 25, this),
+ target_(target) {
+ }
+ virtual ~FavIconCrashAnimation() {}
+
+ // Animation overrides:
+ virtual void AnimateToState(double state) {
+ const double kHidingOffset = 27;
+
+ if (state < .5) {
+ target_->SetFavIconHidingOffset(
+ static_cast<int>(floor(kHidingOffset * 2.0 * state)));
+ } else {
+ target_->DisplayCrashedFavIcon();
+ target_->SetFavIconHidingOffset(
+ static_cast<int>(
+ floor(kHidingOffset - ((state - .5) * 2.0 * kHidingOffset))));
+ }
+ }
+
+ // AnimationDelegate overrides:
+ virtual void AnimationCanceled(const Animation* animation) {
+ target_->SetFavIconHidingOffset(0);
+ }
+
+ private:
+ TabRenderer* target_;
+
+ DISALLOW_EVIL_CONSTRUCTORS(FavIconCrashAnimation);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+// TabRenderer, public:
+
+TabRenderer::TabRenderer()
+ : animation_state_(ANIMATION_NONE),
+ animation_frame_(0),
+ showing_icon_(false),
+ showing_download_icon_(false),
+ showing_close_button_(false),
+ crash_animation_(NULL),
+ fav_icon_hiding_offset_(0),
+ should_display_crashed_favicon_(false) {
+ InitResources();
+
+ // Add the Close Button.
+ close_button_ = new TabCloseButton;
+ close_button_->SetImage(ChromeViews::Button::BS_NORMAL, close_button_n);
+ close_button_->SetImage(ChromeViews::Button::BS_HOT, close_button_h);
+ close_button_->SetImage(ChromeViews::Button::BS_PUSHED, close_button_p);
+ AddChildView(close_button_);
+
+ hover_animation_.reset(new SlideAnimation(this));
+ hover_animation_->SetSlideDuration(kHoverDurationMs);
+}
+
+TabRenderer::~TabRenderer() {
+ delete crash_animation_;
+}
+
+void TabRenderer::UpdateData(TabContents* contents) {
+ DCHECK(contents);
+ data_.favicon = contents->GetFavIcon();
+ data_.title = contents->GetTitle();
+ data_.loading = contents->is_loading();
+ data_.off_the_record = contents->profile()->IsOffTheRecord();
+ data_.show_icon = contents->ShouldDisplayFavIcon();
+ data_.show_download_icon = contents->IsDownloadShelfVisible();
+ data_.crashed = contents->IsCrashed();
+}
+
+void TabRenderer::UpdateFromModel() {
+ // Force a layout, since the tab may have grown a favicon.
+ Layout();
+ SchedulePaint();
+
+ if (data_.crashed) {
+ if (!should_display_crashed_favicon_ && !IsPerformingCrashAnimation())
+ StartCrashAnimation();
+ } else {
+ if (IsPerformingCrashAnimation())
+ StopCrashAnimation();
+ ResetCrashedFavIcon();
+ }
+}
+
+bool TabRenderer::IsSelected() const {
+ return true;
+}
+
+void TabRenderer::ValidateLoadingAnimation(AnimationState animation_state) {
+ if (animation_state_ != animation_state) {
+ // The waiting animation is the reverse of the loading animation, but at a
+ // different rate - the following reverses and scales the animation_frame_
+ // so that the frame is at an equivalent position when going from one
+ // animation to the other.
+ if (animation_state_ == ANIMATION_WAITING &&
+ animation_state == ANIMATION_LOADING) {
+ animation_frame_ = loading_animation_frame_count -
+ (animation_frame_ / waiting_to_loading_frame_count_ratio);
+ }
+ animation_state_ = animation_state;
+ }
+
+ if (animation_state_ != ANIMATION_NONE) {
+ animation_frame_ = ++animation_frame_ %
+ ((animation_state_ == ANIMATION_WAITING) ?
+ waiting_animation_frame_count :
+ loading_animation_frame_count);
+ } else {
+ animation_frame_ = 0;
+ }
+
+ SchedulePaint();
+}
+
+void TabRenderer::AnimationProgressed(const Animation* animation) {
+ if (animation == hover_animation_.get()) {
+ SchedulePaint();
+ }
+}
+
+// static
+gfx::Size TabRenderer::GetMinimumSize() {
+ InitResources();
+
+ gfx::Size minimum_size;
+ minimum_size.set_width(kLeftPadding + kRightPadding);
+ // Since we use bitmap images, the real minimum height of the image is
+ // defined most accurately by the height of the end cap images.
+ minimum_size.set_height(tab_active_l->height());
+ return minimum_size;
+}
+
+// static
+gfx::Size TabRenderer::GetMinimumSelectedSize() {
+ gfx::Size minimum_size = GetMinimumSize();
+ minimum_size.set_width(kLeftPadding + kFaviconSize + kRightPadding);
+ return minimum_size;
+}
+
+// static
+gfx::Size TabRenderer::GetStandardSize() {
+ gfx::Size standard_size = GetMinimumSize();
+ standard_size.set_width(
+ standard_size.width() + kFavIconTitleSpacing + kStandardTitleWidth);
+ return standard_size;
+}
+
+// static
+void TabRenderer::FormatTitleForDisplay(std::wstring* title) {
+ size_t current_index = 0;
+ size_t match_index;
+ while ((match_index = title->find(L'\n', current_index)) !=
+ std::wstring::npos) {
+ title->replace(match_index, 1, L"");
+ current_index = match_index;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// TabRenderer, protected:
+
+std::wstring TabRenderer::GetTitle() const {
+ return data_.title;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// TabRenderer, ChromeViews::View overrides:
+
+void TabRenderer::Paint(ChromeCanvas* canvas) {
+ // Don't paint if we're narrower than we can render correctly. (This should
+ // only happen during animations).
+ if (GetWidth() < GetMinimumSize().width())
+ return;
+
+ // See if the model changes whether the icons should be painted.
+ const bool show_icon = ShouldShowIcon();
+ const bool show_download_icon = data_.show_download_icon;
+ const bool show_close_button = ShouldShowCloseBox();
+ if (show_icon != showing_icon_ ||
+ show_download_icon != showing_download_icon_ ||
+ show_close_button != showing_close_button_)
+ Layout();
+
+ if (IsSelected()) {
+ // Sometimes detaching a tab quickly can result in the model reporting it
+ // as not being selected, so is_drag_clone_ ensures that we always paint
+ // the active representation for the dragged tab.
+ PaintActiveTabBackground(canvas);
+ } else {
+ // Draw our hover state.
+ if (hover_animation_->GetCurrentValue() > 0) {
+ PaintHoverTabBackground(canvas, hover_animation_->GetCurrentValue() *
+ (win_util::ShouldUseVistaFrame() ?
+ kHoverOpacityVista : kHoverOpacity));
+ } else {
+ PaintInactiveTabBackground(canvas);
+ }
+ }
+
+ // Paint the loading animation if the page is currently loading, otherwise
+ // show the page's favicon.
+ if (show_icon) {
+ if (animation_state_ != ANIMATION_NONE) {
+ PaintLoadingAnimation(canvas);
+ } else {
+ canvas->save();
+ canvas->ClipRectInt(0, 0, GetWidth(), GetHeight() - 4);
+ if (should_display_crashed_favicon_) {
+ canvas->DrawBitmapInt(*crashed_fav_icon, 0, 0,
+ crashed_fav_icon->width(),
+ crashed_fav_icon->height(),
+ favicon_bounds_.x(),
+ favicon_bounds_.y() + fav_icon_hiding_offset_,
+ kFaviconSize, kFaviconSize,
+ true);
+ } else {
+ if (!data_.favicon.isNull()) {
+ canvas->DrawBitmapInt(data_.favicon, 0, 0,
+ data_.favicon.width(),
+ data_.favicon.height(),
+ favicon_bounds_.x(),
+ favicon_bounds_.y() + fav_icon_hiding_offset_,
+ kFaviconSize, kFaviconSize,
+ true);
+ }
+ }
+ canvas->restore();
+ }
+ }
+
+ if (show_download_icon) {
+ canvas->DrawBitmapInt(*download_icon,
+ download_icon_bounds_.x(), download_icon_bounds_.y());
+ }
+
+ // Paint the Title.
+ std::wstring title = data_.title;
+ if (title.empty()) {
+ if (data_.loading) {
+ title = l10n_util::GetString(IDS_TAB_LOADING_TITLE);
+ } else {
+ title = l10n_util::GetString(IDS_TAB_UNTITLED_TITLE);
+ }
+ } else {
+ FormatTitleForDisplay(&title);
+ }
+
+ SkColor title_color = IsSelected() ? kSelectedTitleColor
+ : kUnselectedTitleColor;
+ canvas->DrawStringInt(title, title_font, title_color, title_bounds_.x(),
+ title_bounds_.y(), title_bounds_.width(),
+ title_bounds_.height());
+}
+
+void TabRenderer::Layout() {
+ CRect lb;
+ GetLocalBounds(&lb, false);
+ if (lb.IsRectEmpty())
+ return;
+
+ lb.left += kLeftPadding;
+ lb.top += kTopPadding;
+ lb.bottom -= kBottomPadding;
+ lb.right -= kRightPadding;
+
+ // First of all, figure out who is tallest.
+ int content_height = GetContentHeight();
+
+ // Size the Favicon.
+ showing_icon_ = ShouldShowIcon();
+ if (showing_icon_) {
+ int favicon_top = kTopPadding + (content_height - kFaviconSize) / 2;
+ favicon_bounds_.SetRect(lb.left, favicon_top, kFaviconSize, kFaviconSize);
+ } else {
+ favicon_bounds_.SetRect(lb.left, lb.top, 0, 0);
+ }
+
+ // Size the download icon.
+ showing_download_icon_ = data_.show_download_icon;
+ if (showing_download_icon_) {
+ int icon_top = kTopPadding + (content_height - download_icon_height) / 2;
+ download_icon_bounds_.SetRect(lb.Width() - download_icon_width, icon_top,
+ download_icon_width, download_icon_height);
+ }
+
+ // Size the Close button.
+ showing_close_button_ = ShouldShowCloseBox();
+ if (showing_close_button_) {
+ int close_button_top =
+ kTopPadding + kCloseButtonVertFuzz +
+ (content_height - close_button_height) / 2;
+ // If the ratio of the close button size to tab width exceeds the maximum.
+ close_button_->SetBounds(lb.Width() + kCloseButtonHorzFuzz,
+ close_button_top, close_button_width,
+ close_button_height);
+ close_button_->SetVisible(true);
+ } else {
+ close_button_->SetBounds(0, 0, 0, 0);
+ close_button_->SetVisible(false);
+ }
+
+ // Size the Title text to fill the remaining space.
+ int title_left = favicon_bounds_.right() + kFavIconTitleSpacing;
+ int title_top = kTopPadding + (content_height - title_font_height) / 2;
+
+ // If the user has big fonts, the title will appear rendered too far down on
+ // the y-axis if we use the regular top padding, so we need to adjust it so
+ // that the text appears centered.
+ gfx::Size minimum_size = GetMinimumSize();
+ int text_height = title_top + title_font_height + kBottomPadding;
+ if (text_height > minimum_size.height())
+ title_top -= (text_height - minimum_size.height()) / 2;
+
+ int title_width;
+ if (close_button_->IsVisible()) {
+ title_width = std::max(close_button_->GetX() -
+ kTitleCloseButtonSpacing - title_left, 0);
+ } else {
+ title_width = std::max(lb.Width() - title_left, 0);
+ }
+ if (data_.show_download_icon)
+ title_width = std::max(title_width - download_icon_width, 0);
+ title_bounds_.SetRect(title_left, title_top, title_width, title_font_height);
+
+ // Certain UI elements within the Tab (the favicon, the download icon, etc.)
+ // are not represented as child Views (which is the preferred method).
+ // Instead, these UI elements are drawn directly on the canvas from within
+ // Tab::Paint(). The Tab's child Views (for example, the Tab's close button
+ // which is a ChromeViews::Button instance) are automatically mirrored by the
+ // mirroring infrastructure in ChromeViews. The elements Tab draws directly
+ // on the canvas need to be manually mirrored if the View's layout is
+ // right-to-left.
+ favicon_bounds_.set_x(MirroredLeftPointForRect(favicon_bounds_));
+ title_bounds_.set_x(MirroredLeftPointForRect(title_bounds_));
+ download_icon_bounds_.set_x(MirroredLeftPointForRect(download_icon_bounds_));
+}
+
+void TabRenderer::DidChangeBounds(const CRect& previous,
+ const CRect& current) {
+ Layout();
+}
+
+
+void TabRenderer::OnMouseEntered(const ChromeViews::MouseEvent& e) {
+ hover_animation_->SetTweenType(SlideAnimation::EASE_OUT);
+ hover_animation_->Show();
+}
+
+void TabRenderer::OnMouseExited(const ChromeViews::MouseEvent& e) {
+ hover_animation_->SetTweenType(SlideAnimation::EASE_IN);
+ hover_animation_->Hide();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// TabRenderer, private
+
+void TabRenderer::PaintInactiveTabBackground(ChromeCanvas* canvas) {
+ bool is_otr = data_.off_the_record;
+ canvas->DrawBitmapInt(is_otr ? *tab_inactive_otr_l : *tab_inactive_l, 0, 0);
+ canvas->TileImageInt(is_otr ? *tab_inactive_otr_c : *tab_inactive_c,
+ tab_inactive_l_width, 0,
+ GetWidth() - tab_inactive_l_width - tab_inactive_r_width,
+ GetHeight());
+ canvas->DrawBitmapInt(is_otr ? *tab_inactive_otr_r : *tab_inactive_r,
+ GetWidth() - tab_inactive_r_width, 0);
+}
+
+void TabRenderer::PaintActiveTabBackground(ChromeCanvas* canvas) {
+ canvas->DrawBitmapInt(*tab_active_l, 0, 0);
+ canvas->TileImageInt(*tab_active_c, tab_active_l_width, 0,
+ GetWidth() - tab_active_l_width - tab_active_r_width, GetHeight());
+ canvas->DrawBitmapInt(*tab_active_r, GetWidth() - tab_active_r_width, 0);
+}
+
+void TabRenderer::PaintHoverTabBackground(ChromeCanvas* canvas,
+ double opacity) {
+ bool is_otr = data_.off_the_record;
+ SkBitmap left = gfx::ImageOperations::CreateBlendedBitmap(
+ (is_otr ? *tab_inactive_otr_l : *tab_inactive_l),
+ *tab_hover_l, opacity);
+ SkBitmap center = gfx::ImageOperations::CreateBlendedBitmap(
+ (is_otr ? *tab_inactive_otr_c : *tab_inactive_c),
+ *tab_hover_c, opacity);
+ SkBitmap right = gfx::ImageOperations::CreateBlendedBitmap(
+ (is_otr ? *tab_inactive_otr_r : *tab_inactive_r),
+ *tab_hover_r, opacity);
+
+ canvas->DrawBitmapInt(left, 0, 0);
+ canvas->TileImageInt(center, tab_active_l_width, 0,
+ GetWidth() - tab_active_l_width - tab_active_r_width, GetHeight());
+ canvas->DrawBitmapInt(right, GetWidth() - tab_active_r_width, 0);
+}
+
+void TabRenderer::PaintLoadingAnimation(ChromeCanvas* canvas) {
+ SkBitmap* frames = (animation_state_ == ANIMATION_WAITING) ?
+ waiting_animation_frames : loading_animation_frames;
+ int image_size = frames->height();
+ int image_offset = animation_frame_ * image_size;
+ int dst_y = (GetHeight() - image_size) / 2;
+
+ // Just like with the Tab's title and favicon, the position for the page
+ // loading animation also needs to be mirrored if the View's UI layout is
+ // right-to-left.
+ int dst_x;
+ if (UILayoutIsRightToLeft()) {
+ dst_x = GetWidth() - kLeftPadding - image_size;
+ } else {
+ dst_x = kLeftPadding;
+ }
+ canvas->DrawBitmapInt(*frames, image_offset, 0, image_size,
+ image_size, dst_x, dst_y, image_size, image_size,
+ false);
+}
+
+int TabRenderer::IconCapacity() const {
+ if (GetHeight() < GetMinimumSize().height()) {
+ return 0;
+ }
+ return (GetWidth() - kLeftPadding - kRightPadding) / kFaviconSize;
+}
+
+bool TabRenderer::ShouldShowIcon() const {
+ if (!data_.show_icon) {
+ return false;
+ } else if (IsSelected()) {
+ // The selected tab clips favicon before close button.
+ return IconCapacity() >= 2;
+ }
+ // Non-selected tabs clip close button before favicon.
+ return IconCapacity() >= 1;
+}
+
+bool TabRenderer::ShouldShowCloseBox() const {
+ // The selected tab never clips close button.
+ return IsSelected() || IconCapacity() >= 3;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// TabRenderer, private:
+
+void TabRenderer::StartCrashAnimation() {
+ if (!crash_animation_)
+ crash_animation_ = new FavIconCrashAnimation(this);
+ crash_animation_->Reset();
+ crash_animation_->Start();
+}
+
+void TabRenderer::StopCrashAnimation() {
+ if (!crash_animation_)
+ return;
+ crash_animation_->Stop();
+}
+
+bool TabRenderer::IsPerformingCrashAnimation() const {
+ return crash_animation_ && crash_animation_->IsAnimating();
+}
+
+void TabRenderer::SetFavIconHidingOffset(int offset) {
+ fav_icon_hiding_offset_ = offset;
+ SchedulePaint();
+}
+
+void TabRenderer::DisplayCrashedFavIcon() {
+ should_display_crashed_favicon_ = true;
+}
+
+void TabRenderer::ResetCrashedFavIcon() {
+ should_display_crashed_favicon_ = false;
+}
diff --git a/chrome/browser/tabs/tab_renderer.h b/chrome/browser/tabs/tab_renderer.h
new file mode 100644
index 0000000..90e6f9f
--- /dev/null
+++ b/chrome/browser/tabs/tab_renderer.h
@@ -0,0 +1,192 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef CHROME_BROWSER_TABS_TAB_RENDERER_H__
+#define CHROME_BROWSER_TABS_TAB_RENDERER_H__
+
+#include "base/gfx/point.h"
+#include "chrome/common/animation.h"
+#include "chrome/common/slide_animation.h"
+#include "chrome/common/notification_service.h"
+#include "chrome/views/button.h"
+#include "chrome/views/menu.h"
+#include "chrome/views/view.h"
+
+class TabContents;
+
+///////////////////////////////////////////////////////////////////////////////
+//
+// TabRenderer
+//
+// A View that renders a Tab, either in a TabStrip or in a DraggedTabView.
+//
+///////////////////////////////////////////////////////////////////////////////
+class TabRenderer : public ChromeViews::View,
+ public AnimationDelegate {
+ public:
+ // Possible animation states.
+ enum AnimationState {
+ ANIMATION_NONE,
+ ANIMATION_WAITING,
+ ANIMATION_LOADING
+ };
+
+ TabRenderer();
+ virtual ~TabRenderer();
+
+ // Updates the data the Tab uses to render itself from the specified
+ // TabContents.
+ void UpdateData(TabContents* contents);
+
+ // Updates the display to reflect the contents of this TabRenderer's model.
+ void UpdateFromModel();
+
+ // Returns true if the Tab is selected, false otherwise.
+ virtual bool IsSelected() const;
+
+ // Advance the Loading Animation to the next frame, or hide the animation if
+ // the tab isn't loading.
+ void ValidateLoadingAnimation(AnimationState animation_state);
+
+ // AnimationDelegate implementation.
+ virtual void AnimationProgressed(const Animation* animation);
+
+ // Returns the minimum possible size of a single unselected Tab.
+ static gfx::Size GetMinimumSize();
+ // Returns the minimum possible size of a selected Tab. Selected tabs must
+ // always show a close button and have a larger minimum size than unselected
+ // tabs.
+ static gfx::Size GetMinimumSelectedSize();
+ // Returns the preferred size of a single Tab, assuming space is
+ // available.
+ static gfx::Size GetStandardSize();
+
+ // Remove invalid characters from the title (e.g. newlines) that may
+ // interfere with rendering.
+ static void FormatTitleForDisplay(std::wstring* title);
+
+ protected:
+ ChromeViews::Button* close_button() const { return close_button_; }
+ const gfx::Rect& title_bounds() const { return title_bounds_; }
+
+ // Returns the title of the Tab.
+ std::wstring GetTitle() const;
+
+ private:
+ // Overridden from ChromeViews::View:
+ virtual void Paint(ChromeCanvas* canvas);
+ virtual void Layout();
+ virtual void DidChangeBounds(const CRect& previous, const CRect& current);
+ virtual void OnMouseEntered(const ChromeViews::MouseEvent& event);
+ virtual void OnMouseExited(const ChromeViews::MouseEvent& event);
+
+ // Starts/Stops the crash animation.
+ void StartCrashAnimation();
+ void StopCrashAnimation();
+
+ // Return true if the crash animation is currently running.
+ bool IsPerformingCrashAnimation() const;
+
+ // Set the temporary offset for the favicon. This is used during animation.
+ void SetFavIconHidingOffset(int offset);
+
+ void DisplayCrashedFavIcon();
+ void ResetCrashedFavIcon();
+
+ // Paint various portions of the Tab
+ void PaintInactiveTabBackground(ChromeCanvas* canvas);
+ void PaintActiveTabBackground(ChromeCanvas* canvas);
+ void PaintHoverTabBackground(ChromeCanvas* canvas, double opacity);
+ void PaintLoadingAnimation(ChromeCanvas* canvas);
+
+ // Returns the number of favicon-size elements that can fit in the tab's
+ // current size.
+ int IconCapacity() const;
+
+ // Returns whether the Tab should display a favicon.
+ bool ShouldShowIcon() const;
+
+ // Returns whether the Tab should display a close button.
+ bool ShouldShowCloseBox() const;
+
+ // The bounds of various sections of the display.
+ gfx::Rect favicon_bounds_;
+ gfx::Rect download_icon_bounds_;
+ gfx::Rect title_bounds_;
+
+ // Current state of the animation.
+ AnimationState animation_state_;
+
+ // The current index into the Animation image strip.
+ int animation_frame_;
+
+ // Close Button.
+ ChromeViews::Button* close_button_;
+
+ // Hover animation.
+ scoped_ptr<SlideAnimation> hover_animation_;
+
+ // Model data. We store this here so that we don't need to ask the underlying
+ // model, which is tricky since instances of this object can outlive the
+ // corresponding objects in the underlying model.
+ struct TabData {
+ SkBitmap favicon;
+ std::wstring title;
+ bool loading;
+ bool crashed;
+ bool off_the_record;
+ bool show_icon;
+ bool show_download_icon;
+ };
+ TabData data_;
+
+ // Whether we're showing the icon. It is cached so that we can detect when it
+ // changes and layout appropriately.
+ bool showing_icon_;
+
+ // Whether we are showing the download icon. Comes from the model.
+ bool showing_download_icon_;
+
+ // Whether we are showing the close button. It is cached so that we can
+ // detect when it changes and layout appropriately.
+ bool showing_close_button_;
+
+ // The offset used to animate the favicon location.
+ int fav_icon_hiding_offset_;
+
+ // The animation object used to swap the favicon with the sad tab icon.
+ class FavIconCrashAnimation;
+ FavIconCrashAnimation* crash_animation_;
+
+ bool should_display_crashed_favicon_;
+
+ DISALLOW_EVIL_CONSTRUCTORS(TabRenderer);
+};
+
+#endif CHROME_BROWSER_TABS_TAB_RENDERER_H__
diff --git a/chrome/browser/tabs/tab_strip.cc b/chrome/browser/tabs/tab_strip.cc
new file mode 100644
index 0000000..40233b5
--- /dev/null
+++ b/chrome/browser/tabs/tab_strip.cc
@@ -0,0 +1,1385 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "chrome/browser/tabs/tab_strip.h"
+
+#include "base/gfx/size.h"
+#include "chrome/app/theme/theme_resources.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/tab_contents.h"
+#include "chrome/browser/tabs/dragged_tab_controller.h"
+#include "chrome/browser/tabs/tab.h"
+#include "chrome/browser/tabs/tab_strip_model.h"
+#include "chrome/browser/user_metrics.h"
+#include "chrome/browser/view_ids.h"
+#include "chrome/browser/vista_frame.h"
+#include "chrome/browser/web_contents.h"
+#include "chrome/common/gfx/chrome_canvas.h"
+#include "chrome/common/l10n_util.h"
+#include "chrome/common/os_exchange_data.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/common/resource_bundle.h"
+#include "chrome/common/slide_animation.h"
+#include "chrome/common/stl_util-inl.h"
+#include "chrome/common/win_util.h"
+#include "chrome/views/image_view.h"
+#include "chrome/views/painter.h"
+
+#include "generated_resources.h"
+
+#undef min
+#undef max
+
+using ChromeViews::DropTargetEvent;
+
+static const int kDefaultAnimationDurationMs = 100;
+static const int kResizeLayoutAnimationDurationMs = 166;
+static const int kReorderAnimationDurationMs = 166;
+
+static const int kLoadingAnimationFrameTimeMs = 30;
+static const int kNewTabButtonHOffset = -5;
+static const int kNewTabButtonVOffset = 5;
+static const int kResizeTabsTimeMs = 300;
+static const int kSuspendAnimationsTimeMs = 200;
+static const int kTabHOffset = -16;
+static const int kTabStripAnimationVSlop = 40;
+
+// Size of the drop indicator.
+static int drop_indicator_width;
+static int drop_indicator_height;
+
+static inline int Round(double x) {
+ // Why oh why is this not in a standard header?
+ return static_cast<int>(floor(x + 0.5));
+}
+
+///////////////////////////////////////////////////////////////////////////////
+//
+// TabAnimation
+//
+// A base class for all TabStrip animations.
+//
+class TabStrip::TabAnimation : public AnimationDelegate {
+ public:
+ friend class TabStrip;
+
+ TabAnimation(TabStrip* tabstrip)
+ : tabstrip_(tabstrip),
+ animation_(this),
+ start_selected_width_(0),
+ start_unselected_width_(0),
+ end_selected_width_(0),
+ end_unselected_width_(0),
+ layout_on_completion_(false) {
+ }
+ virtual ~TabAnimation() {}
+
+ void Start() {
+ animation_.SetSlideDuration(GetDuration());
+ animation_.SetTweenType(SlideAnimation::EASE_OUT);
+ if (!animation_.IsShowing()) {
+ animation_.Reset();
+ animation_.Show();
+ }
+ }
+
+ void Stop() {
+ animation_.Stop();
+ }
+
+ void set_layout_on_completion(bool layout_on_completion) {
+ layout_on_completion_ = layout_on_completion;
+ }
+
+ // Retrieves the width for the Tab at the specified index if an animation is
+ // active.
+ static double GetCurrentTabWidth(TabStrip* tabstrip,
+ TabStrip::TabAnimation* animation,
+ int index) {
+ double unselected, selected;
+ tabstrip->GetCurrentTabWidths(&unselected, &selected);
+ Tab* tab = tabstrip->GetTabAt(index);
+ double tab_width = tab->IsSelected() ? selected : unselected;
+ if (animation) {
+ double specified_tab_width = animation->GetWidthForTab(index);
+ if (specified_tab_width != -1)
+ tab_width = specified_tab_width;
+ }
+ return tab_width;
+ }
+
+ // Overridden from AnimationDelegate:
+ virtual void AnimationProgressed(const Animation* animation) {
+ tabstrip_->AnimationLayout(end_unselected_width_);
+ }
+
+ virtual void AnimationEnded(const Animation* animation) {
+ tabstrip_->FinishAnimation(this, layout_on_completion_);
+ // This object is destroyed now, so we can't do anything else after this.
+ }
+
+ virtual void AnimationCanceled(const Animation* animation) {
+ AnimationEnded(animation);
+ }
+
+ protected:
+ // Returns the duration of the animation.
+ virtual int GetDuration() const {
+ return kDefaultAnimationDurationMs;
+ }
+
+ // Subclasses override to return the width of the Tab at the specified index
+ // at the current animation frame. -1 indicates the default width should be
+ // used for the Tab.
+ virtual double GetWidthForTab(int index) const {
+ return -1; // Use default.
+ }
+
+ // Figure out the desired start and end widths for the specified pre- and
+ // post- animation tab counts.
+ void GenerateStartAndEndWidths(int start_tab_count, int end_tab_count) {
+ tabstrip_->GetDesiredTabWidths(start_tab_count, &start_unselected_width_,
+ &start_selected_width_);
+ tabstrip_->GenerateIdealBounds();
+ tabstrip_->GetDesiredTabWidths(end_tab_count,
+ &end_unselected_width_,
+ &end_selected_width_);
+ }
+
+ TabStrip* tabstrip_;
+ SlideAnimation animation_;
+
+ double start_selected_width_;
+ double start_unselected_width_;
+ double end_selected_width_;
+ double end_unselected_width_;
+
+ private:
+ // True if a complete re-layout is required upon completion of the animation.
+ // Subclasses set this if they don't perform a complete layout
+ // themselves and canceling the animation may leave the strip in an
+ // inconsistent state.
+ bool layout_on_completion_;
+
+ DISALLOW_EVIL_CONSTRUCTORS(TabAnimation);
+};
+
+///////////////////////////////////////////////////////////////////////////////
+
+// Handles insertion of a Tab at |index|.
+class InsertTabAnimation : public TabStrip::TabAnimation {
+ public:
+ explicit InsertTabAnimation(TabStrip* tabstrip, int index)
+ : TabAnimation(tabstrip),
+ index_(index) {
+ int tab_count = tabstrip->GetTabCount();
+ GenerateStartAndEndWidths(tab_count - 1, tab_count);
+ }
+ virtual ~InsertTabAnimation() {}
+
+ protected:
+ // Overridden from TabStrip::TabAnimation:
+ virtual double GetWidthForTab(int index) const {
+ if (index == index_) {
+ bool is_selected = tabstrip_->model()->selected_index() == index;
+ double target_width =
+ is_selected ? end_unselected_width_ : end_selected_width_;
+ double start_width = is_selected ? Tab::GetMinimumSelectedSize().width() :
+ Tab::GetMinimumSize().width();
+ double delta = target_width - start_width;
+ if (delta > 0)
+ return start_width + (delta * animation_.GetCurrentValue());
+ return start_width;
+ }
+ if (tabstrip_->GetTabAt(index)->IsSelected()) {
+ double delta = end_selected_width_ - start_selected_width_;
+ return start_selected_width_ + (delta * animation_.GetCurrentValue());
+ }
+ double delta = end_unselected_width_ - start_unselected_width_;
+ return start_unselected_width_ + (delta * animation_.GetCurrentValue());
+ }
+
+ private:
+ int index_;
+
+ DISALLOW_EVIL_CONSTRUCTORS(InsertTabAnimation);
+};
+
+///////////////////////////////////////////////////////////////////////////////
+
+// Handles removal of a Tab from |index|
+class RemoveTabAnimation : public TabStrip::TabAnimation {
+ public:
+ RemoveTabAnimation(TabStrip* tabstrip, int index, TabContents* contents)
+ : TabAnimation(tabstrip),
+ index_(index) {
+ int tab_count = tabstrip->GetTabCount();
+ GenerateStartAndEndWidths(tab_count, tab_count - 1);
+ }
+
+ virtual ~RemoveTabAnimation() {
+ }
+
+ protected:
+ // Overridden from TabStrip::TabAnimation:
+ virtual double GetWidthForTab(int index) const {
+ Tab* tab = tabstrip_->GetTabAt(index);
+ if (index == index_) {
+ // The tab(s) being removed are gradually shrunken depending on the state
+ // of the animation.
+ // Removed animated Tabs are never selected.
+ double start_width = start_unselected_width_;
+ double target_width = Tab::GetMinimumSize().width() + kTabHOffset;
+ double delta = start_width - target_width;
+ return start_width - (delta * animation_.GetCurrentValue());
+ }
+ if (tabstrip_->available_width_for_tabs_ != -1 &&
+ index_ != tabstrip_->GetTabCount() - 1) {
+ return TabStrip::TabAnimation::GetWidthForTab(index);
+ }
+ // All other tabs are sized according to the start/end widths specified at
+ // the start of the animation.
+ if (tab->IsSelected()) {
+ double delta = end_selected_width_ - start_selected_width_;
+ return start_selected_width_ + (delta * animation_.GetCurrentValue());
+ }
+ double delta = end_unselected_width_ - start_unselected_width_;
+ return start_unselected_width_ + (delta * animation_.GetCurrentValue());
+ }
+
+ virtual void AnimationEnded(const Animation* animation) {
+ RemoveTabAt(index_);
+ HighlightCloseButton();
+ TabStrip::TabAnimation::AnimationEnded(animation);
+ }
+
+ private:
+ // Cleans up the Tab from the TabStrip at the specified |index| once its
+ // animated removal is complete.
+ void RemoveTabAt(int index) const {
+ // Save a pointer to the Tab before we remove the TabData, we'll need this
+ // later.
+ Tab* removed = tabstrip_->tab_data_.at(index).tab;
+
+ // Remove the Tab from the TabStrip's list...
+ tabstrip_->tab_data_.erase(tabstrip_->tab_data_.begin() + index);
+
+ // If the TabContents being detached was removed as a result of a drag
+ // gesture from its corresponding Tab, we don't want to remove the Tab from
+ // the child list, because if we do so it'll stop receiving events and the
+ // drag will stall. So we only remove if a drag isn't active, or the Tab
+ // was for some other TabContents.
+ if (!tabstrip_->IsDragSessionActive() ||
+ !tabstrip_->drag_controller_->IsDragSourceTab(removed)) {
+ tabstrip_->RemoveChildView(removed);
+ delete removed;
+ }
+ }
+
+ // When the animation completes, we send the ViewContainer a message to
+ // simulate a mouse moved event at the current mouse position. This tickles
+ // the Tab the mouse is currently over to show the "hot" state of the close
+ // button.
+ void HighlightCloseButton() {
+ if (tabstrip_->available_width_for_tabs_ == -1 ||
+ tabstrip_->IsDragSessionActive()) {
+ // This function is not required (and indeed may crash!) for removes
+ // spawned by non-mouse closes and drag-detaches.
+ return;
+ }
+
+ POINT pt;
+ GetCursorPos(&pt);
+ ChromeViews::ViewContainer* vc = tabstrip_->GetViewContainer();
+ RECT wr;
+ GetWindowRect(vc->GetHWND(), &wr);
+ pt.x -= wr.left;
+ pt.y -= wr.top;
+
+ // Return to message loop - otherwise we may disrupt some operation that's
+ // in progress.
+ PostMessage(vc->GetHWND(), WM_MOUSEMOVE, 0, MAKELPARAM(pt.x, pt.y));
+ }
+
+ int index_;
+
+ DISALLOW_EVIL_CONSTRUCTORS(RemoveTabAnimation);
+};
+
+///////////////////////////////////////////////////////////////////////////////
+
+// Handles the movement of a Tab from one position to another.
+class MoveTabAnimation : public TabStrip::TabAnimation {
+ public:
+ MoveTabAnimation(TabStrip* tabstrip, int tab_a_index, int tab_b_index)
+ : TabAnimation(tabstrip),
+ start_tab_a_bounds_(tabstrip_->GetIdealBounds(tab_b_index)),
+ start_tab_b_bounds_(tabstrip_->GetIdealBounds(tab_a_index)) {
+ tab_a_ = tabstrip_->GetTabAt(tab_a_index);
+ tab_b_ = tabstrip_->GetTabAt(tab_b_index);
+
+ // Since we don't do a full TabStrip re-layout, we need to force a full
+ // layout upon completion since we're not guaranteed to be in a good state
+ // if for example the animation is canceled.
+ set_layout_on_completion(true);
+ }
+ virtual ~MoveTabAnimation() {}
+
+ // Overridden from AnimationDelegate:
+ virtual void AnimationProgressed(const Animation* animation) {
+ // Position Tab A
+ double distance = start_tab_b_bounds_.x() - start_tab_a_bounds_.x();
+ double delta = distance * animation_.GetCurrentValue();
+ double new_x = start_tab_a_bounds_.x() + delta;
+ tab_a_->SetBounds(Round(new_x), tab_a_->GetY(), tab_a_->GetWidth(),
+ tab_a_->GetHeight());
+
+ // Position Tab B
+ distance = start_tab_a_bounds_.x() - start_tab_b_bounds_.x();
+ delta = distance * animation_.GetCurrentValue();
+ new_x = start_tab_b_bounds_.x() + delta;
+ tab_b_->SetBounds(Round(new_x), tab_b_->GetY(), tab_b_->GetWidth(),
+ tab_b_->GetHeight());
+
+ tabstrip_->SchedulePaint();
+ }
+
+ protected:
+ // Overridden from TabStrip::TabAnimation:
+ virtual int GetDuration() const { return kReorderAnimationDurationMs; }
+
+ private:
+ // The two tabs being exchanged.
+ Tab* tab_a_;
+ Tab* tab_b_;
+
+ // ...and their bounds.
+ gfx::Rect start_tab_a_bounds_;
+ gfx::Rect start_tab_b_bounds_;
+
+ DISALLOW_EVIL_CONSTRUCTORS(MoveTabAnimation);
+};
+
+///////////////////////////////////////////////////////////////////////////////
+
+// Handles the animated resize layout of the entire TabStrip from one width
+// to another.
+class ResizeLayoutAnimation : public TabStrip::TabAnimation {
+ public:
+ ResizeLayoutAnimation(TabStrip* tabstrip)
+ : TabAnimation(tabstrip) {
+ int tab_count = tabstrip->GetTabCount();
+ GenerateStartAndEndWidths(tab_count, tab_count);
+ InitStartState();
+ }
+ virtual ~ResizeLayoutAnimation() {
+ }
+
+ // Overridden from AnimationDelegate:
+ virtual void AnimationEnded(const Animation* animation) {
+ tabstrip_->resize_layout_scheduled_ = false;
+ TabStrip::TabAnimation::AnimationEnded(animation);
+ }
+
+ protected:
+ // Overridden from TabStrip::TabAnimation:
+ virtual int GetDuration() const {
+ return kResizeLayoutAnimationDurationMs;
+ }
+
+ virtual double GetWidthForTab(int index) const {
+ if (tabstrip_->GetTabAt(index)->IsSelected()) {
+ double delta = end_selected_width_ - start_selected_width_;
+ return start_selected_width_ + (delta * animation_.GetCurrentValue());
+ }
+ double delta = end_unselected_width_ - start_unselected_width_;
+ return start_unselected_width_ + (delta * animation_.GetCurrentValue());
+ }
+
+ private:
+ // We need to start from the current widths of the Tabs as they were last
+ // laid out, _not_ the last known good state, which is what'll be done if we
+ // don't measure the Tab sizes here and just go with the default TabAnimation
+ // behavior...
+ void InitStartState() {
+ for (int i = 0; i < tabstrip_->GetTabCount(); ++i) {
+ Tab* current_tab = tabstrip_->GetTabAt(i);
+ if (current_tab->IsSelected()) {
+ start_selected_width_ = current_tab->GetWidth();
+ } else {
+ start_unselected_width_ = current_tab->GetWidth();
+ }
+ }
+ }
+
+ DISALLOW_EVIL_CONSTRUCTORS(ResizeLayoutAnimation);
+};
+
+///////////////////////////////////////////////////////////////////////////////
+// TabStrip, public:
+
+TabStrip::TabStrip(TabStripModel* model)
+ : model_(model),
+ resize_layout_factory_(this),
+ added_as_message_loop_observer_(false),
+ resize_layout_scheduled_(false),
+ current_unselected_width_(Tab::GetStandardSize().width()),
+ current_selected_width_(Tab::GetStandardSize().width()),
+ available_width_for_tabs_(-1) {
+ Init();
+}
+
+TabStrip::~TabStrip() {
+ // Stop any existing Loading Animation timer.
+ MessageLoop::current()->timer_manager()->StopTimer(
+ loading_animation_timer_.get());
+
+ // Make sure we unhook ourselves as a message loop observer so that we don't
+ // crash in the case where the user closes the window after closing a tab
+ // but before moving the mouse.
+ RemoveMessageLoopObserver();
+}
+
+int TabStrip::GetPreferredHeight() {
+ CSize preferred_size;
+ GetPreferredSize(&preferred_size);
+ return preferred_size.cy;
+}
+
+bool TabStrip::HasAvailableDragActions() const {
+ return model_->delegate()->GetDragActions() != 0;
+}
+
+void TabStrip::ShowApplicationMenu(const gfx::Point& p) {
+ TabStripModelDelegate* delegate = model_->delegate();
+ if (delegate)
+ delegate->ShowApplicationMenu(p);
+}
+
+bool TabStrip::CanProcessInputEvents() const {
+ return IsAnimating() == NULL;
+}
+
+bool TabStrip::IsCompatibleWith(TabStrip* other) {
+ return model_->profile() == other->model()->profile();
+}
+
+bool TabStrip::IsAnimating() const {
+ return active_animation_.get() != NULL;
+}
+
+void TabStrip::DestroyDragController() {
+ if (IsDragSessionActive())
+ drag_controller_.reset(NULL);
+}
+
+gfx::Rect TabStrip::GetIdealBounds(int index) {
+ DCHECK(index >= 0 && index < GetTabCount());
+ return tab_data_.at(index).ideal_bounds;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// TabStrip, ChromeViews::View overrides:
+
+void TabStrip::PaintChildren(ChromeCanvas* canvas) {
+ // Paint the tabs in reverse order, so they stack to the left.
+ Tab* selected_tab = NULL;
+ for (int i = GetTabCount() - 1; i >= 0; --i) {
+ Tab* tab = GetTabAt(i);
+ // We must ask the _Tab's_ model, not ourselves, because in some situations
+ // the model will be different to this object, e.g. when a Tab is being
+ // removed after its TabContents has been destroyed.
+ if (!tab->IsSelected()) {
+ tab->ProcessPaint(canvas);
+ } else {
+ selected_tab = tab;
+ }
+ }
+
+ if (win_util::ShouldUseVistaFrame()) {
+ // Make sure unselected tabs are somewhat transparent.
+ SkPaint paint;
+ paint.setColor(SkColorSetARGB(200, 255, 255, 255));
+ paint.setPorterDuffXfermode(SkPorterDuff::kDstIn_Mode);
+ paint.setStyle(SkPaint::kFill_Style);
+ canvas->FillRectInt(
+ 0, 0, GetWidth(),
+ GetHeight() - 2, // Visible region that overlaps the toolbar.
+ paint);
+ }
+
+ // Paint the selected tab last, so it overlaps all the others.
+ if (selected_tab)
+ selected_tab->ProcessPaint(canvas);
+
+ // Paint the New Tab button.
+ newtab_button_->ProcessPaint(canvas);
+}
+
+void TabStrip::DidChangeBounds(const CRect& prev, const CRect& curr) {
+ Layout();
+}
+
+// Overridden to support automation. See automation_proxy_uitest.cc.
+ChromeViews::View* TabStrip::GetViewByID(int view_id) const {
+ if (GetTabCount() > 0) {
+ if (view_id == VIEW_ID_TAB_LAST) {
+ return GetTabAt(GetTabCount() - 1);
+ } else if ((view_id >= VIEW_ID_TAB_0) && (view_id < VIEW_ID_TAB_LAST)) {
+ int index = view_id - VIEW_ID_TAB_0;
+ if (index >= 0 && index < GetTabCount()) {
+ return GetTabAt(index);
+ } else {
+ return NULL;
+ }
+ }
+ }
+
+ return View::GetViewByID(view_id);
+}
+
+void TabStrip::Layout() {
+ // Called from:
+ // - window resize
+ // - animation completion
+ if (active_animation_.get())
+ active_animation_->Stop();
+ GenerateIdealBounds();
+ int tab_count = GetTabCount();
+ int tab_right = 0;
+ for (int i = 0; i < tab_count; ++i) {
+ const gfx::Rect& bounds = tab_data_.at(i).ideal_bounds;
+ GetTabAt(i)->SetBounds(bounds.x(), bounds.y(), bounds.width(),
+ bounds.height());
+ tab_right = bounds.right() + kTabHOffset;
+ }
+ LayoutNewTabButton(static_cast<double>(tab_right), current_unselected_width_);
+ SchedulePaint();
+}
+
+void TabStrip::GetPreferredSize(CSize* preferred_size) {
+ DCHECK(preferred_size);
+ preferred_size->cx = 0;
+ preferred_size->cy = Tab::GetMinimumSize().height();
+}
+
+void TabStrip::OnDragEntered(const DropTargetEvent& event) {
+ UpdateDropIndex(event);
+}
+
+int TabStrip::OnDragUpdated(const DropTargetEvent& event) {
+ UpdateDropIndex(event);
+ return GetDropEffect(event);
+}
+
+void TabStrip::OnDragExited() {
+ SetDropIndex(-1, false);
+}
+
+int TabStrip::OnPerformDrop(const DropTargetEvent& event) {
+ if (!drop_info_.get())
+ return DragDropTypes::DRAG_NONE;
+
+ const int drop_index = drop_info_->drop_index;
+ const bool drop_before = drop_info_->drop_before;
+
+ // Hide the drop indicator.
+ SetDropIndex(-1, false);
+
+ GURL url;
+ std::wstring title;
+ if (!event.GetData().GetURLAndTitle(&url, &title) || !url.is_valid())
+ return DragDropTypes::DRAG_NONE;
+
+ if (drop_before) {
+ UserMetrics::RecordAction(L"Tab_DropURLBetweenTabs", model_->profile());
+
+ // Insert a new tab.
+ TabContents* contents =
+ model_->delegate()->CreateTabContentsForURL(
+ url, model_->profile(), PageTransition::TYPED, false, NULL);
+ model_->AddTabContents(contents, drop_index, PageTransition::GENERATED,
+ true);
+ } else {
+ UserMetrics::RecordAction(L"Tab_DropURLOnTab", model_->profile());
+
+ model_->GetTabContentsAt(drop_index)->controller()->
+ LoadURL(url, PageTransition::GENERATED);
+ model_->SelectTabContentsAt(drop_index, true);
+ }
+
+ return GetDropEffect(event);
+}
+
+bool TabStrip::GetAccessibleRole(VARIANT* role) {
+ DCHECK(role);
+
+ role->vt = VT_I4;
+ role->lVal = ROLE_SYSTEM_GROUPING;
+ return true;
+}
+
+bool TabStrip::GetAccessibleName(std::wstring* name) {
+ if (!accessible_name_.empty()) {
+ (*name).assign(accessible_name_);
+ return true;
+ }
+ return false;
+}
+
+void TabStrip::SetAccessibleName(const std::wstring& name) {
+ accessible_name_.assign(name);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// TabStrip, TabStripModelObserver implementation:
+
+void TabStrip::TabInsertedAt(TabContents* contents,
+ int index,
+ bool foreground) {
+ DCHECK(contents);
+ DCHECK(index == TabStripModel::kNoTab || model_->ContainsIndex(index));
+
+ if (active_animation_.get())
+ active_animation_->Stop();
+
+ Tab* tab = NULL;
+ // First see if this Tab is one that was dragged out of this TabStrip and is
+ // now being dragged back in. In this case, the DraggedTabController actually
+ // has the Tab already constructed and we can just insert it into our list
+ // again.
+ if (IsDragSessionActive()) {
+ tab = drag_controller_->GetDragSourceTabForContents(contents);
+ if (tab) {
+ // If the Tab was detached, it would have been animated closed but not
+ // removed, so we need to reset this property.
+ tab->set_closing(false);
+ tab->ValidateLoadingAnimation(TabRenderer::ANIMATION_NONE);
+ }
+ }
+
+ // Otherwise we need to make a new Tab.
+ if (!tab)
+ tab = new Tab(this);
+
+ if (index == TabStripModel::kNoTab) {
+ TabData d = { tab, gfx::Rect() };
+ tab_data_.push_back(d);
+ tab->UpdateData(contents);
+ } else {
+ TabData d = { tab, gfx::Rect() };
+ tab_data_.insert(tab_data_.begin() + index, d);
+ tab->UpdateData(contents);
+ }
+ // We only add the tab to the child list if it's not already - an invisible
+ // tab maintained by the DraggedTabController will already be parented.
+ if (!tab->GetParent())
+ AddChildView(tab);
+
+ // Don't animate the first tab, it looks weird, and don't animate anything
+ // if the containing window isn't visible yet.
+ if (GetTabCount() > 1 && IsWindowVisible(GetViewContainer()->GetHWND())) {
+ StartInsertTabAnimation(index);
+ } else {
+ Layout();
+ }
+}
+
+void TabStrip::TabDetachedAt(TabContents* contents, int index) {
+ if (CanUpdateDisplay()) {
+ GenerateIdealBounds();
+ StartRemoveTabAnimation(index, contents);
+ // Have to do this _after_ calling StartRemoveTabAnimation, so that any
+ // previous remove is completed fully and index is valid in sync with the
+ // model index.
+ GetTabAt(index)->set_closing(true);
+ }
+}
+
+void TabStrip::TabSelectedAt(TabContents* old_contents,
+ TabContents* new_contents,
+ int index,
+ bool user_gesture) {
+ DCHECK(index >= 0 && index < GetTabCount());
+ if (CanUpdateDisplay()) {
+ // We have "tiny tabs" if the tabs are so tiny that the unselected ones are
+ // a different size to the selected ones.
+ bool tiny_tabs = current_unselected_width_ != current_selected_width_;
+ if (!IsAnimating() && (!resize_layout_scheduled_ || tiny_tabs)) {
+ Layout();
+ } else {
+ SchedulePaint();
+ }
+ }
+}
+
+void TabStrip::TabMoved(TabContents* contents, int from_index, int to_index) {
+ Tab* tab = GetTabAt(from_index);
+ Tab* other_tab = GetTabAt(to_index);
+ tab_data_.erase(tab_data_.begin() + from_index);
+ TabData data = {tab, gfx::Rect()};
+ tab_data_.insert(tab_data_.begin() + to_index, data);
+ GenerateIdealBounds();
+ StartMoveTabAnimation(from_index, to_index);
+}
+
+void TabStrip::TabChangedAt(TabContents* contents, int index) {
+ Tab* tab = GetTabAt(index);
+ tab->UpdateData(contents);
+ tab->UpdateFromModel();
+}
+
+void TabStrip::TabValidateAnimations() {
+ TimerManager* tm = MessageLoop::current()->timer_manager();
+ Timer* timer = loading_animation_timer_.get();
+ if (model_->TabsAreLoading()) {
+ if (!tm->IsTimerRunning(timer)) {
+ // Loads are happening, and the timer isn't running, so start it.
+ tm->ResetTimer(timer);
+ }
+ } else {
+ if (tm->IsTimerRunning(timer)) {
+ // Loads are now complete, update the state if a task was scheduled.
+ LoadingAnimationCallback();
+ tm->StopTimer(timer);
+ }
+ }
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// TabStrip, Tab::Delegate implementation:
+
+bool TabStrip::IsTabSelected(const Tab* tab) const {
+ if (tab->closing())
+ return false;
+
+ int tab_count = GetTabCount();
+ for (int i = 0, index = 0; i < tab_count; ++i, ++index) {
+ Tab* current_tab = GetTabAt(i);
+ if (current_tab->closing())
+ --index;
+ if (current_tab == tab)
+ return index == model_->selected_index();
+ }
+ return false;
+}
+
+void TabStrip::SelectTab(Tab* tab) {
+ int index = GetIndexOfTab(tab);
+ if (index != -1)
+ model_->SelectTabContentsAt(index, true);
+}
+
+void TabStrip::CloseTab(Tab* tab) {
+ int tab_index = GetIndexOfTab(tab);
+ if (tab_index != -1) {
+ UserMetrics::RecordAction(L"CloseTab_Mouse",
+ model_->GetTabContentsAt(tab_index)->profile());
+ Tab* last_tab = GetTabAt(GetTabCount() - 1);
+ // Limit the width available to the TabStrip for laying out Tabs, so that
+ // Tabs are not resized until a later time (when the mouse pointer leaves
+ // the TabStrip).
+ available_width_for_tabs_ = GetAvailableWidthForTabs(last_tab);
+ resize_layout_scheduled_ = true;
+ AddMessageLoopObserver();
+ model_->CloseTabContentsAt(tab_index);
+ }
+}
+
+bool TabStrip::IsCommandEnabledForTab(
+ TabStripModel::ContextMenuCommand command_id, const Tab* tab) const {
+ int index = GetIndexOfTab(tab);
+ if (index != -1)
+ return model_->IsContextMenuCommandEnabled(index, command_id);
+ return false;
+}
+
+void TabStrip::ExecuteCommandForTab(
+ TabStripModel::ContextMenuCommand command_id, Tab* tab) {
+ int index = GetIndexOfTab(tab);
+ if (index != -1)
+ model_->ExecuteContextMenuCommand(index, command_id);
+}
+
+void TabStrip::MaybeStartDrag(Tab* tab, const ChromeViews::MouseEvent& event) {
+ // Don't accidentally start any drag operations during animations if the
+ // mouse is down... during an animation tabs are being resized automatically,
+ // so the View system can misinterpret this easily if the mouse is down that
+ // the user is dragging.
+ if (IsAnimating() || tab->closing())
+ return;
+ drag_controller_.reset(new DraggedTabController(tab, this));
+ drag_controller_->CaptureDragInfo(gfx::Point(event.GetX(), event.GetY()));
+}
+
+void TabStrip::ContinueDrag(const ChromeViews::MouseEvent& event) {
+ // We can get called even if |MaybeStartDrag| wasn't called in the event of
+ // a TabStrip animation when the mouse button is down. In this case we should
+ // _not_ continue the drag because it can lead to weird bugs.
+ if (drag_controller_.get())
+ drag_controller_->Drag();
+}
+
+void TabStrip::EndDrag(bool canceled) {
+ if (drag_controller_.get())
+ drag_controller_->EndDrag(canceled);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// TabStrip, ChromeViews::BaseButton::ButtonListener implementation:
+
+void TabStrip::ButtonPressed(ChromeViews::BaseButton* sender) {
+ if (sender == newtab_button_)
+ model_->AddBlankTab(true);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// TabStrip, Task implementation:
+
+void TabStrip::Run() {
+ // Loading Animation frame advancement timer has fired, update all of the
+ // loading animations as applicable...
+ LoadingAnimationCallback();
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// TabStrip, MessageLoop::Observer implementation:
+
+void TabStrip::WillProcessMessage(const MSG& msg) {
+}
+
+void TabStrip::DidProcessMessage(const MSG& msg) {
+ // We spy on three different Windows messages here to see if the mouse has
+ // moved out of the bounds of the tabstrip, which we use as our cue to kick
+ // of the resize animation. The messages are:
+ //
+ // WM_MOUSEMOVE:
+ // For when the mouse moves from the tabstrip over into the rest of the
+ // browser UI, i.e. within the bounds of the same windows HWND.
+ // WM_MOUSELEAVE:
+ // For when the mouse moves very rapidly from a tab closed in the middle of
+ // the tabstrip (_not_ the end) out of the bounds of the browser's HWND and
+ // over some other HWND.
+ // WM_NCMOUSELEAVE:
+ // For when the mouse moves very rapidly from the end of the tabstrip (when
+ // the last tab is closed and the mouse is left floating over the title
+ // bar). Because the empty area of the tabstrip at the end of the title bar
+ // is registered by the ChromeFrame as part of the "caption" area of the
+ // window (the frame's OnNCHitTest method returns HTCAPTION for this
+ // region), the frame's HWND receives a WM_MOUSEMOVE message immediately,
+ // because as far as it is concerned the mouse has _left_ the client area
+ // of the window (and is now over the non-client area). To be notified
+ // again when the mouse leaves the _non-client_ area, we use the
+ // WM_NCMOUSELEAVE message, which causes us to re-evaluate the cursor
+ // position and correctly resize the tabstrip.
+ //
+ switch (msg.message) {
+ case WM_MOUSEMOVE:
+ case WM_MOUSELEAVE:
+ case WM_NCMOUSELEAVE:
+ if (!IsCursorInTabStripZone()) {
+ // Mouse moved outside the tab slop zone, start a timer to do a resize
+ // layout after a short while...
+ if (resize_layout_factory_.empty()) {
+ MessageLoop::current()->PostDelayedTask(FROM_HERE,
+ resize_layout_factory_.NewRunnableMethod(
+ &TabStrip::ResizeLayoutTabs),
+ kResizeTabsTimeMs);
+ }
+ } else {
+ // Mouse moved quickly out of the tab strip and then into it again, so
+ // cancel the timer so that the strip doesn't move when the mouse moves
+ // back over it.
+ resize_layout_factory_.RevokeAll();
+ }
+ break;
+ }
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// TabStrip, private:
+
+void TabStrip::Init() {
+ model_->AddObserver(this);
+ newtab_button_ = new ChromeViews::Button;
+ newtab_button_->SetListener(this, TabStripModel::kNoTab);
+ ResourceBundle& rb = ResourceBundle::GetSharedInstance();
+ SkBitmap* bitmap;
+
+ bitmap = rb.GetBitmapNamed(IDR_NEWTAB_BUTTON);
+ newtab_button_->SetImage(ChromeViews::Button::BS_NORMAL, bitmap);
+ newtab_button_->SetImage(ChromeViews::Button::BS_PUSHED,
+ rb.GetBitmapNamed(IDR_NEWTAB_BUTTON_P));
+ newtab_button_->SetImage(ChromeViews::Button::BS_HOT,
+ rb.GetBitmapNamed(IDR_NEWTAB_BUTTON_H));
+
+ newtab_button_size_.SetSize(bitmap->width(), bitmap->height());
+ actual_newtab_button_size_ = newtab_button_size_;
+
+ newtab_button_->SetAccessibleName(l10n_util::GetString(IDS_ACCNAME_NEWTAB));
+ AddChildView(newtab_button_);
+
+ // Creating the Timer directly instead of using StartTimer() ensures it won't
+ // actually start running until we use ResetTimer();
+ loading_animation_timer_.reset(
+ new Timer(kLoadingAnimationFrameTimeMs, this, true));
+
+ if (drop_indicator_width == 0) {
+ // Direction doesn't matter, both images are the same size.
+ SkBitmap* drop_image = GetDropArrowImage(true);
+ drop_indicator_width = drop_image->width();
+ drop_indicator_height = drop_image->height();
+ }
+}
+
+Tab* TabStrip::GetTabAt(int index) const {
+ DCHECK(index >= 0 && index < GetTabCount());
+ return tab_data_.at(index).tab;
+}
+
+int TabStrip::GetTabCount() const {
+ return static_cast<int>(tab_data_.size());
+}
+
+void TabStrip::GetCurrentTabWidths(double* unselected_width,
+ double* selected_width) const {
+ *unselected_width = current_unselected_width_;
+ *selected_width = current_selected_width_;
+}
+
+void TabStrip::GetDesiredTabWidths(int tab_count,
+ double* unselected_width,
+ double* selected_width) const {
+ const double min_unselected_width = Tab::GetMinimumSize().width();
+ const double min_selected_width = Tab::GetMinimumSelectedSize().width();
+ if (tab_count == 0) {
+ // Return immediately to avoid divide-by-zero below.
+ *unselected_width = min_unselected_width;
+ *selected_width = min_selected_width;
+ return;
+ }
+
+ // Determine how much space we can actually allocate to tabs.
+ int available_width;
+ if (available_width_for_tabs_ < 0) {
+ available_width = GetWidth();
+ available_width -= (kNewTabButtonHOffset + newtab_button_size_.width());
+ } else {
+ // Interesting corner case: if |available_width_for_tabs_| > the result
+ // of the calculation in the conditional arm above, the strip is in
+ // overflow. We can either use the specified width or the true available
+ // width here; the first preserves the consistent "leave the last tab under
+ // the user's mouse so they can close many tabs" behavior at the cost of
+ // prolonging the glitchy appearance of the overflow state, while the second
+ // gets us out of overflow as soon as possible but forces the user to move
+ // their mouse for a few tabs' worth of closing. We choose visual
+ // imperfection over behavioral imperfection and select the first option.
+ available_width = available_width_for_tabs_;
+ }
+
+ // Calculate the desired tab widths by dividing the available space into equal
+ // portions. Don't let tabs get larger than the "standard width" or smaller
+ // than the minimum width for each type, respectively.
+ const int total_offset = kTabHOffset * (tab_count - 1);
+ const double desired_tab_width = std::min((static_cast<double>(
+ available_width - total_offset) / static_cast<double>(tab_count)),
+ static_cast<double>(Tab::GetStandardSize().width()));
+ *unselected_width = std::max(desired_tab_width, min_unselected_width);
+ *selected_width = std::max(desired_tab_width, min_selected_width);
+
+ // When there are multiple tabs, we'll have one selected and some unselected
+ // tabs. If the desired width was between the minimum sizes of these types,
+ // try to shrink the tabs with the smaller minimum. For example, if we have
+ // a strip of width 10 with 4 tabs, the desired width per tab will be 2.5. If
+ // selected tabs have a minimum width of 4 and unselected tabs have a minimum
+ // width of 1, the above code would set *unselected_width = 2.5,
+ // *selected_width = 4, which results in a total width of 11.5. Instead, we
+ // want to set *unselected_width = 2, *selected_width = 4, for a total width
+ // of 10.
+ if (tab_count > 1) {
+ if ((min_unselected_width < min_selected_width) &&
+ (desired_tab_width < min_selected_width)) {
+ // Unselected width = (total width - selected width) / (num_tabs - 1)
+ *unselected_width = std::max(static_cast<double>(
+ available_width - total_offset - min_selected_width) /
+ static_cast<double>(tab_count - 1), min_unselected_width);
+ } else if ((min_unselected_width > min_selected_width) &&
+ (desired_tab_width < min_unselected_width)) {
+ // Selected width = (total width - (unselected width * (num_tabs - 1)))
+ *selected_width = std::max(available_width - total_offset -
+ (min_unselected_width * (tab_count - 1)), min_selected_width);
+ }
+ }
+}
+
+void TabStrip::ResizeLayoutTabs() {
+ resize_layout_factory_.RevokeAll();
+
+ // It is critically important that this is unhooked here, otherwise we will
+ // keep spying on messages forever.
+ RemoveMessageLoopObserver();
+
+ available_width_for_tabs_ = -1;
+ double unselected, selected;
+ GetDesiredTabWidths(GetTabCount(), &unselected, &selected);
+ Tab* first_tab = GetTabAt(0);
+ int w = Round(first_tab->IsSelected() ? selected : selected);
+
+ // We only want to run the animation if we're not already at the desired
+ // size.
+ if (abs(first_tab->GetWidth() - w) > 1)
+ StartResizeLayoutAnimation();
+}
+
+bool TabStrip::IsCursorInTabStripZone() {
+ CRect bounds;
+ GetLocalBounds(&bounds, true);
+ CPoint tabstrip_topleft = bounds.TopLeft();
+ View::ConvertPointToScreen(this, &tabstrip_topleft);
+ bounds.MoveToXY(tabstrip_topleft);
+ bounds.bottom += kTabStripAnimationVSlop;
+
+ CPoint cursor_point;
+ GetCursorPos(&cursor_point);
+
+ return !!bounds.PtInRect(cursor_point);
+}
+
+void TabStrip::AddMessageLoopObserver() {
+ if (!added_as_message_loop_observer_) {
+ MessageLoop::current()->AddObserver(this);
+ added_as_message_loop_observer_ = true;
+ }
+}
+
+void TabStrip::RemoveMessageLoopObserver() {
+ if (added_as_message_loop_observer_) {
+ MessageLoop::current()->RemoveObserver(this);
+ added_as_message_loop_observer_ = false;
+ }
+}
+
+void TabStrip::LoadingAnimationCallback() {
+ for (int i = 0, index = 0; i < GetTabCount(); ++i, ++index) {
+ Tab* current_tab = GetTabAt(i);
+ if (current_tab->closing()) {
+ --index;
+ } else {
+ TabContents* contents = model_->GetTabContentsAt(index);
+ if (!contents || !contents->is_loading()) {
+ current_tab->ValidateLoadingAnimation(Tab::ANIMATION_NONE);
+ } else if (contents->response_started()) {
+ current_tab->ValidateLoadingAnimation(Tab::ANIMATION_WAITING);
+ } else {
+ current_tab->ValidateLoadingAnimation(Tab::ANIMATION_LOADING);
+ }
+ }
+ }
+
+ // Make sure the model delegates updates the animation as well.
+ TabStripModelDelegate* delegate;
+ if (model_ && (delegate = model_->delegate()))
+ delegate->ValidateLoadingAnimations();
+}
+
+gfx::Rect TabStrip::GetDropBounds(int drop_index,
+ bool drop_before,
+ bool* is_beneath) {
+ DCHECK(drop_index != -1);
+ int center_x;
+ if (drop_index < GetTabCount()) {
+ Tab* tab = GetTabAt(drop_index);
+ if (drop_before)
+ center_x = tab->GetX() - (kTabHOffset / 2);
+ else
+ center_x = tab->GetX() + (tab->GetWidth() / 2);
+ } else {
+ Tab* last_tab = GetTabAt(drop_index - 1);
+ center_x = last_tab->GetX() + last_tab->GetWidth() + (kTabHOffset / 2);
+ }
+
+ // Mirror the center point if necessary.
+ center_x = MirroredXCoordinateInsideView(center_x);
+
+ // Determine the screen bounds.
+ CPoint drop_loc(center_x - drop_indicator_width / 2, -drop_indicator_height);
+ ConvertPointToScreen(this, &drop_loc);
+ gfx::Rect drop_bounds(drop_loc.x, drop_loc.y, drop_indicator_width,
+ drop_indicator_height);
+
+ // If the rect doesn't fit on the monitor, push the arrow to the bottom.
+ gfx::Rect monitor_bounds = win_util::GetMonitorBoundsForRect(drop_bounds);
+ *is_beneath = (monitor_bounds.IsEmpty() ||
+ !monitor_bounds.Contains(drop_bounds));
+ if (*is_beneath)
+ drop_bounds.Offset(0, drop_bounds.height() + GetHeight());
+
+ return drop_bounds;
+}
+
+void TabStrip::UpdateDropIndex(const DropTargetEvent& event) {
+ // If the UI layout is right-to-left, we need to mirror the mouse
+ // coordinates since we calculate the drop index based on the
+ // original (and therefore non-mirrored) positions of the tabs.
+ const int x = MirroredXCoordinateInsideView(event.GetX());
+ for (int i = 0; i < GetTabCount(); ++i) {
+ Tab* tab = GetTabAt(i);
+ const int tab_max_x = tab->GetX() + tab->GetWidth();
+ const int hot_width = tab->GetWidth() / 3;
+ if (x < tab_max_x) {
+ if (x < tab->GetX() + hot_width)
+ SetDropIndex(i, true);
+ else if (x >= tab_max_x - hot_width)
+ SetDropIndex(i + 1, true);
+ else
+ SetDropIndex(i, false);
+ return;
+ }
+ }
+
+ // The drop isn't over a tab, add it to the end.
+ SetDropIndex(GetTabCount(), true);
+}
+
+void TabStrip::SetDropIndex(int index, bool drop_before) {
+ if (index == -1) {
+ if (drop_info_.get())
+ drop_info_.reset(NULL);
+ return;
+ }
+
+ if (drop_info_.get() && drop_info_->drop_index == index &&
+ drop_info_->drop_before == drop_before) {
+ return;
+ }
+
+ bool is_beneath;
+ gfx::Rect drop_bounds = GetDropBounds(index, drop_before, &is_beneath);
+
+ if (!drop_info_.get()) {
+ drop_info_.reset(new DropInfo(index, drop_before, !is_beneath));
+ } else {
+ drop_info_->drop_index = index;
+ drop_info_->drop_before = drop_before;
+ if (is_beneath == drop_info_->point_down) {
+ drop_info_->point_down = !is_beneath;
+ drop_info_->arrow_view->SetImage(
+ GetDropArrowImage(drop_info_->point_down));
+ }
+ }
+
+ // Reposition the window. Need to show it too as the window is initially
+ // hidden.
+
+ drop_info_->arrow_window->SetWindowPos(
+ HWND_TOPMOST, drop_bounds.x(), drop_bounds.y(), drop_bounds.width(),
+ drop_bounds.height(), SWP_NOACTIVATE | SWP_SHOWWINDOW);
+}
+
+int TabStrip::GetDropEffect(const ChromeViews::DropTargetEvent& event) {
+ const int source_ops = event.GetSourceOperations();
+ if (source_ops & DragDropTypes::DRAG_COPY)
+ return DragDropTypes::DRAG_COPY;
+ if (source_ops & DragDropTypes::DRAG_LINK)
+ return DragDropTypes::DRAG_LINK;
+ return DragDropTypes::DRAG_MOVE;
+}
+
+// static
+SkBitmap* TabStrip::GetDropArrowImage(bool is_down) {
+ return ResourceBundle::GetSharedInstance().GetBitmapNamed(
+ is_down ? IDR_TAB_DROP_DOWN : IDR_TAB_DROP_UP);
+}
+
+// TabStrip::DropInfo ----------------------------------------------------------
+
+TabStrip::DropInfo::DropInfo(int drop_index, bool drop_before, bool point_down)
+ : drop_index(drop_index),
+ drop_before(drop_before),
+ point_down(point_down) {
+ arrow_window = new ChromeViews::HWNDViewContainer();
+ arrow_window->set_window_style(WS_POPUP);
+ arrow_window->set_window_ex_style(WS_EX_TOPMOST | WS_EX_NOACTIVATE |
+ WS_EX_LAYERED | WS_EX_TRANSPARENT);
+
+ arrow_view = new ChromeViews::ImageView;
+ arrow_view->SetImage(GetDropArrowImage(point_down));
+
+ arrow_window->Init(
+ NULL,
+ gfx::Rect(0, 0, drop_indicator_width, drop_indicator_height),
+ arrow_view,
+ true);
+}
+
+TabStrip::DropInfo::~DropInfo() {
+ // Close eventually deletes the window, which deletes arrow_view too.
+ arrow_window->Close();
+}
+
+///////////////////////////////////////////////////////////////////////////////
+
+// Called from:
+// - BasicLayout
+// - Tab insertion/removal
+// - Tab reorder
+void TabStrip::GenerateIdealBounds() {
+ int tab_count = GetTabCount();
+ double unselected, selected;
+ GetDesiredTabWidths(tab_count, &unselected, &selected);
+
+ current_unselected_width_ = unselected;
+ current_selected_width_ = selected;
+
+ // NOTE: This currently assumes a tab's height doesn't differ based on
+ // selected state or the number of tabs in the strip!
+ int tab_height = Tab::GetStandardSize().height();
+ double tab_x = 0;
+ for (int i = 0; i < tab_count; ++i) {
+ Tab* tab = GetTabAt(i);
+ double tab_width = unselected;
+ if (tab->IsSelected())
+ tab_width = selected;
+ double end_of_tab = tab_x + tab_width;
+ int rounded_tab_x = Round(tab_x);
+ gfx::Rect state(rounded_tab_x, 0, Round(end_of_tab) - rounded_tab_x,
+ tab_height);
+ tab_data_.at(i).ideal_bounds = state;
+ tab_x = end_of_tab + kTabHOffset;
+ }
+}
+
+void TabStrip::LayoutNewTabButton(double last_tab_right,
+ double unselected_width) {
+ int delta = abs(Round(unselected_width) - Tab::GetStandardSize().width());
+ if (delta > 1 && !resize_layout_scheduled_) {
+ // We're shrinking tabs, so we need to anchor the New Tab button to the
+ // right edge of the TabStrip's bounds, rather than the right edge of the
+ // right-most Tab, otherwise it'll bounce when animating.
+ newtab_button_->SetBounds(GetWidth() - newtab_button_size_.width(),
+ kNewTabButtonVOffset,
+ newtab_button_size_.width(),
+ newtab_button_size_.height());
+ } else {
+ newtab_button_->SetBounds(
+ Round(last_tab_right - kTabHOffset) + kNewTabButtonHOffset,
+ kNewTabButtonVOffset, newtab_button_size_.width(),
+ newtab_button_size_.height());
+ }
+}
+
+// Called from:
+// - animation tick
+void TabStrip::AnimationLayout(double unselected_width) {
+ int tab_height = Tab::GetStandardSize().height();
+ double tab_x = 0;
+ for (int i = 0; i < GetTabCount(); ++i) {
+ TabAnimation* animation = active_animation_.get();
+ double tab_width = TabAnimation::GetCurrentTabWidth(this, animation, i);
+ double end_of_tab = tab_x + tab_width;
+ int rounded_tab_x = Round(tab_x);
+ Tab* tab = GetTabAt(i);
+ tab->SetBounds(rounded_tab_x, 0, Round(end_of_tab) - rounded_tab_x,
+ tab_height);
+ tab_x = end_of_tab + kTabHOffset;
+ }
+ LayoutNewTabButton(tab_x, unselected_width);
+ SchedulePaint();
+}
+
+void TabStrip::StartResizeLayoutAnimation() {
+ if (active_animation_.get())
+ active_animation_->Stop();
+ active_animation_.reset(new ResizeLayoutAnimation(this));
+ active_animation_->Start();
+}
+
+void TabStrip::StartInsertTabAnimation(int index) {
+ // The TabStrip can now use its entire width to lay out Tabs.
+ available_width_for_tabs_ = -1;
+ if (active_animation_.get())
+ active_animation_->Stop();
+ active_animation_.reset(new InsertTabAnimation(this, index));
+ active_animation_->Start();
+}
+
+void TabStrip::StartRemoveTabAnimation(int index, TabContents* contents) {
+ if (active_animation_.get()) {
+ // Some animations (e.g. MoveTabAnimation) cause there to be a Layout when
+ // they're completed (which includes canceled). Since |tab_data_| is now
+ // inconsistent with TabStripModel, doing this Layout will crash now, so
+ // we ask the MoveTabAnimation to skip its Layout (the state will be
+ // corrected by the RemoveTabAnimation we're about to initiate).
+ active_animation_->set_layout_on_completion(false);
+ active_animation_->Stop();
+ }
+ active_animation_.reset(new RemoveTabAnimation(this, index, contents));
+ active_animation_->Start();
+}
+
+void TabStrip::StartMoveTabAnimation(int from_index, int to_index) {
+ if (active_animation_.get())
+ active_animation_->Stop();
+ active_animation_.reset(new MoveTabAnimation(this, from_index, to_index));
+ active_animation_->Start();
+}
+
+bool TabStrip::CanUpdateDisplay() {
+ // Don't bother laying out/painting when we're closing all tabs.
+ if (model_->closing_all()) {
+ // Make sure any active animation is ended, too.
+ if (active_animation_.get())
+ active_animation_->Stop();
+ return false;
+ }
+ return true;
+}
+
+void TabStrip::FinishAnimation(TabStrip::TabAnimation* animation,
+ bool layout) {
+ active_animation_.reset(NULL);
+ if (layout)
+ Layout();
+}
+
+int TabStrip::GetIndexOfTab(const Tab* tab) const {
+ for (int i = 0, index = 0; i < GetTabCount(); ++i, ++index) {
+ Tab* current_tab = GetTabAt(i);
+ if (current_tab->closing()) {
+ --index;
+ } else if (current_tab == tab) {
+ return index;
+ }
+ }
+ return -1;
+}
+
+int TabStrip::GetAvailableWidthForTabs(Tab* last_tab) const {
+ return last_tab->GetX() + last_tab->GetWidth();
+}
diff --git a/chrome/browser/tabs/tab_strip.h b/chrome/browser/tabs/tab_strip.h
new file mode 100644
index 0000000..fbbe83b
--- /dev/null
+++ b/chrome/browser/tabs/tab_strip.h
@@ -0,0 +1,372 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef CHROME_BROWSER_TABS_TAB_STRIP_H__
+#define CHROME_BROWSER_TABS_TAB_STRIP_H__
+
+#include "base/gfx/point.h"
+#include "base/task.h"
+#include "chrome/browser/tabs/tab.h"
+#include "chrome/browser/tabs/tab_strip_model.h"
+#include "chrome/views/button.h"
+#include "chrome/views/hwnd_view_container.h"
+#include "chrome/views/menu.h"
+#include "chrome/views/view.h"
+
+class DraggedTabController;
+class ScopedMouseCloseWidthCalculator;
+class TabStripModel;
+class Timer;
+
+namespace ChromeViews {
+class ImageView;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+//
+// TabStrip
+//
+// A View that represents the TabStripModel. The TabStrip has the
+// following responsibilities:
+// - It implements the TabStripModelObserver interface, and acts as a
+// container for Tabs, and is also responsible for creating them.
+// - It takes part in Tab Drag & Drop with Tab, TabDragHelper and
+// DraggedTab, focusing on tasks that require reshuffling other tabs
+// in response to dragged tabs.
+//
+///////////////////////////////////////////////////////////////////////////////
+class TabStrip : public ChromeViews::View,
+ public TabStripModelObserver,
+ public Tab::TabDelegate,
+ public ChromeViews::Button::ButtonListener,
+ public Task,
+ public MessageLoop::Observer {
+ public:
+ TabStrip(TabStripModel* model);
+ virtual ~TabStrip();
+
+ // Returns the preferred height of this TabStrip. This is based on the
+ // typical height of its constituent tabs.
+ int GetPreferredHeight();
+
+ // Returns true if the associated TabStrip's delegate supports tab moving or
+ // detaching. Used by the Frame to determine if dragging on the Tab
+ // itself should move the window in cases where there's only one
+ // non drag-able Tab.
+ bool HasAvailableDragActions() const;
+
+ // Ask the delegate to show the application menu at the provided point.
+ // The point is in screen coordinate system.
+ void ShowApplicationMenu(const gfx::Point& p);
+
+ // Returns true if the TabStrip can accept input events. This returns false
+ // when the TabStrip is animating to a new state and as such the user should
+ // not be allowed to interact with the TabStrip.
+ bool CanProcessInputEvents() const;
+
+ // Return true if this tab strip is compatible with the provided tab strip.
+ // Compatible tab strips can transfer tabs during drag and drop.
+ bool IsCompatibleWith(TabStrip* other);
+
+ // Returns true if Tabs in this TabStrip are currently changing size or
+ // position.
+ bool IsAnimating() const;
+
+ // Accessors for the model and individual Tabs.
+ TabStripModel* model() { return model_; }
+
+ // Returns true if there is an active drag session.
+ bool IsDragSessionActive() const { return drag_controller_.get() != NULL; }
+
+ // Aborts any active drag session. This is called from XP/VistaFrame's
+ // end session handler to make sure there are no drag sessions in flight that
+ // could prevent the frame from being closed right away.
+ void AbortActiveDragSession() { EndDrag(true); }
+
+ // Destroys the active drag controller.
+ void DestroyDragController();
+
+ // Retrieve the ideal bounds for the Tab at the specified index.
+ gfx::Rect GetIdealBounds(int index);
+
+ // ChromeViews::View overrides:
+ virtual void PaintChildren(ChromeCanvas* canvas);
+ virtual void DidChangeBounds(const CRect& previous, const CRect& current);
+ virtual ChromeViews::View* GetViewByID(int id) const;
+ virtual void Layout();
+ virtual void GetPreferredSize(CSize* preferred_size);
+ // NOTE: the drag and drop methods are invoked from FrameView. This is done to
+ // allow for a drop region that extends outside the bounds of the TabStrip.
+ virtual void OnDragEntered(const ChromeViews::DropTargetEvent& event);
+ virtual int OnDragUpdated(const ChromeViews::DropTargetEvent& event);
+ virtual void OnDragExited();
+ virtual int OnPerformDrop(const ChromeViews::DropTargetEvent& event);
+ virtual bool GetAccessibleRole(VARIANT* role);
+ virtual bool GetAccessibleName(std::wstring* name);
+ virtual void SetAccessibleName(const std::wstring& name);
+
+ protected:
+ // TabStripModelObserver implementation:
+ virtual void TabInsertedAt(TabContents* contents,
+ int index,
+ bool foreground);
+ virtual void TabDetachedAt(TabContents* contents, int index);
+ virtual void TabSelectedAt(TabContents* old_contents,
+ TabContents* contents,
+ int index,
+ bool user_gesture);
+ virtual void TabMoved(TabContents* contents, int from_index, int to_index);
+ virtual void TabChangedAt(TabContents* contents, int index);
+ virtual void TabValidateAnimations();
+
+ // Tab::Delegate implementation:
+ virtual bool IsTabSelected(const Tab* tab) const;
+ virtual void SelectTab(Tab* tab);
+ virtual void CloseTab(Tab* tab);
+ virtual bool IsCommandEnabledForTab(
+ TabStripModel::ContextMenuCommand command_id, const Tab* tab) const;
+ virtual void ExecuteCommandForTab(
+ TabStripModel::ContextMenuCommand command_id, Tab* tab);
+ virtual void MaybeStartDrag(Tab* tab,
+ const ChromeViews::MouseEvent& event);
+ virtual void ContinueDrag(const ChromeViews::MouseEvent& event);
+ virtual void EndDrag(bool canceled);
+
+ // ChromeViews::Button::ButtonListener implementation:
+ virtual void ButtonPressed(ChromeViews::BaseButton* sender);
+
+ // Task implementation:
+ virtual void Run();
+
+ // MessageLoop::Observer implementation:
+ virtual void WillProcessMessage(const MSG& msg);
+ virtual void DidProcessMessage(const MSG& msg);
+
+ private:
+ friend class DraggedTabController;
+ friend class InsertTabAnimation;
+ friend class MoveTabAnimation;
+ friend class RemoveTabAnimation;
+ friend class ResizeLayoutAnimation;
+ friend class SuspendAnimationsTask;
+ friend class TabAnimation;
+
+ TabStrip();
+ void Init();
+
+ // Retrieves the Tab at the specified index.
+ Tab* GetTabAt(int index) const;
+
+ // Gets the number of Tabs in the collection.
+ int GetTabCount() const;
+
+ // -- Tab Resize Layout -----------------------------------------------------
+
+ // Returns the exact (unrounded) current width of each tab.
+ void GetCurrentTabWidths(double* unselected_width,
+ double* selected_width) const;
+
+ // Returns the exact (unrounded) desired width of each tab, based on the
+ // desired strip width and number of tabs. If
+ // |width_of_tabs_for_mouse_close_| is nonnegative we use that value in
+ // calculating the desired strip width; otherwise we use the current width.
+ void GetDesiredTabWidths(int tab_count,
+ double* unselected_width,
+ double* selected_width) const;
+
+ // Perform an animated resize-relayout of the TabStrip immediately.
+ void ResizeLayoutTabs();
+
+ // Returns whether or not the cursor is currently in the "tab strip zone"
+ // which is defined as the region above the TabStrip and a bit below it.
+ // Note: this method cannot be const because |ConvertPointToScreen| is not.
+ // #@*($&(#!!!
+ bool IsCursorInTabStripZone();
+
+ // Ensure that the message loop observer used for event spying is added and
+ // removed appropriately so we can tell when to resize layout the tab strip.
+ void AddMessageLoopObserver();
+ void RemoveMessageLoopObserver();
+
+ // Called to update the frame of the Loading animations.
+ void LoadingAnimationCallback();
+
+ // -- Link Drag & Drop ------------------------------------------------------
+
+ // Returns the bounds to render the drop at, in screen coordinates. Sets
+ // |is_beneath| to indicate whether the arrow is beneath the tab, or above
+ // it.
+ gfx::Rect GetDropBounds(int drop_index, bool drop_before, bool* is_beneath);
+
+ // Updates the location of the drop based on the event.
+ void UpdateDropIndex(const ChromeViews::DropTargetEvent& event);
+
+ // Sets the location of the drop, repainting as necessary.
+ void SetDropIndex(int index, bool drop_before);
+
+ // Returns the drop effect for dropping a URL on the tab strip. This does
+ // not query the data in anyway, it only looks at the source operations.
+ int GetDropEffect(const ChromeViews::DropTargetEvent& event);
+
+ // Returns the image to use for indicating a drop on a tab. If is_down is
+ // true, this returns an arrow pointing down.
+ static SkBitmap* GetDropArrowImage(bool is_down);
+
+ // -- Animations ------------------------------------------------------------
+
+ // Generates the ideal bounds of the TabStrip when all Tabs have finished
+ // animating to their desired position/bounds. This is used by the standard
+ // Layout method and other callers like the DraggedTabController that need
+ // stable representations of Tab positions.
+ void GenerateIdealBounds();
+
+ // Lays out the New Tab button, assuming the right edge of the last Tab on
+ // the TabStrip at |last_tab_right|.
+ void LayoutNewTabButton(double last_tab_right, double unselected_width);
+
+ // A generic Layout method for various classes of TabStrip animations,
+ // including Insert, Remove and Resize Layout cases/
+ void AnimationLayout(double unselected_width);
+
+ // Starts various types of TabStrip animations.
+ void StartResizeLayoutAnimation();
+ void StartInsertTabAnimation(int index);
+ void StartRemoveTabAnimation(int index, TabContents* contents);
+ void StartMoveTabAnimation(int from_index, int to_index);
+
+ // Returns true if detach or select changes in the model should be reflected
+ // in the TabStrip. This returns false if we're closing all tabs in the
+ // TabStrip and so we should prevent updating. This is not const because we
+ // use this as a signal to cancel any active animations.
+ bool CanUpdateDisplay();
+
+ // Notifies the TabStrip that the specified TabAnimation has completed.
+ // Optionally a full Layout will be performed, specified by |layout|.
+ class TabAnimation;
+ void FinishAnimation(TabAnimation* animation, bool layout);
+
+ // Finds the index of the TabContents corresponding to |tab| in our
+ // associated TabStripModel, or -1 if there is none (e.g. the specified |tab|
+ // is being animated closed).
+ int GetIndexOfTab(const Tab* tab) const;
+
+ // Calculates the available width for tabs, assuming a Tab is to be closed.
+ int GetAvailableWidthForTabs(Tab* last_tab) const;
+
+ // -- Member Variables ------------------------------------------------------
+
+ // Our model.
+ TabStripModel* model_;
+
+ // A factory that is used to construct a delayed callback to the
+ // ResizeLayoutTabsNow method.
+ ScopedRunnableMethodFactory<TabStrip> resize_layout_factory_;
+
+ // True if the TabStrip has already been added as a MessageLoop observer.
+ bool added_as_message_loop_observer_;
+
+ // True if a resize layout animation should be run a short delay after the
+ // mouse exits the TabStrip.
+ // TODO(beng): (Cleanup) this would be better named "needs_resize_layout_".
+ bool resize_layout_scheduled_;
+
+ // The timer used to update frames for the Loading Animation.
+ scoped_ptr<Timer> loading_animation_timer_;
+
+ // The "New Tab" button.
+ ChromeViews::Button* newtab_button_;
+ gfx::Size newtab_button_size_;
+ gfx::Size actual_newtab_button_size_;
+
+ // The current widths of various types of tabs. We save these so that, as
+ // users close tabs while we're holding them at the same size, we can lay out
+ // tabs exactly and eliminate the "pixel jitter" we'd get from just leaving
+ // them all at their existing, rounded widths.
+ double current_unselected_width_;
+ double current_selected_width_;
+
+ // If this value is nonnegative, it is used in GetDesiredTabWidths() to
+ // calculate how much space in the tab strip to use for tabs. Most of the
+ // time this will be -1, but while we're handling closing a tab via the mouse,
+ // we'll set this to the edge of the last tab before closing, so that if we
+ // are closing the last tab and need to resize immediately, we'll resize only
+ // back to this width, thus once again placing the last tab under the mouse
+ // cursor.
+ int available_width_for_tabs_;
+
+ // Storage of strings needed for accessibility.
+ std::wstring accessible_name_;
+
+ // Used during a drop session of a url. Tracks the position of the drop as
+ // well as a window used to highlight where the drop occurs.
+ struct DropInfo {
+ DropInfo(int index, bool drop_before, bool paint_down);
+ ~DropInfo();
+
+ // Index of the tab to drop on. If drop_before is true, the drop should
+ // occur between the tab at drop_index - 1 and drop_index.
+ // WARNING: if drop_before is true it is possible this will == tab_count,
+ // which indicates the drop should create a new tab at the end of the tabs.
+ int drop_index;
+ bool drop_before;
+
+ // Direction the arrow should point in. If true, the arrow is displayed
+ // above the tab and points down. If false, the arrow is displayed beneath
+ // the tab and points up.
+ bool point_down;
+
+ // Renders the drop indicator.
+ ChromeViews::HWNDViewContainer* arrow_window;
+ ChromeViews::ImageView* arrow_view;
+
+ private:
+ DISALLOW_EVIL_CONSTRUCTORS(DropInfo);
+ };
+
+ // Valid for the lifetime of a drag over us.
+ scoped_ptr<DropInfo> drop_info_;
+
+ // The controller for a drag initiated from a Tab. Valid for the lifetime of
+ // the drag session.
+ scoped_ptr<DraggedTabController> drag_controller_;
+
+ // The Tabs we contain, and their last generated "good" bounds.
+ struct TabData {
+ Tab* tab;
+ gfx::Rect ideal_bounds;
+ };
+ std::vector<TabData> tab_data_;
+
+ // The currently running animation.
+ scoped_ptr<TabAnimation> active_animation_;
+
+ DISALLOW_EVIL_CONSTRUCTORS(TabStrip);
+};
+
+#endif CHROME_BROWSER_TABS_TAB_STRIP_H__
diff --git a/chrome/browser/tabs/tab_strip_model.cc b/chrome/browser/tabs/tab_strip_model.cc
new file mode 100644
index 0000000..ecb0ab4
--- /dev/null
+++ b/chrome/browser/tabs/tab_strip_model.cc
@@ -0,0 +1,593 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include <algorithm>
+
+#include "base/gfx/point.h"
+#include "base/logging.h"
+#include "chrome/browser/browser.h"
+#include "chrome/browser/browser_about_handler.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/dom_ui/new_tab_ui.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/navigation_controller.h"
+#include "chrome/browser/navigation_entry.h"
+#include "chrome/browser/render_view_host.h"
+#include "chrome/browser/tab_contents_factory.h"
+#include "chrome/browser/tab_restore_service.h"
+#include "chrome/browser/tabs/tab_strip_model.h"
+#include "chrome/browser/tabs/tab_strip_model_order_controller.h"
+#include "chrome/browser/user_metrics.h"
+#include "chrome/common/notification_service.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/common/pref_service.h"
+#include "chrome/common/stl_util-inl.h"
+
+///////////////////////////////////////////////////////////////////////////////
+// TabStripModel, public:
+
+TabStripModel::TabStripModel(TabStripModelDelegate* delegate, Profile* profile)
+ : delegate_(delegate),
+ profile_(profile),
+ selected_index_(kNoTab),
+ closing_all_(false),
+ order_controller_(NULL) {
+ NotificationService::current()->AddObserver(this,
+ NOTIFY_TAB_CONTENTS_DESTROYED, NotificationService::AllSources());
+ SetOrderController(new TabStripModelOrderController(this));
+}
+
+TabStripModel::~TabStripModel() {
+ STLDeleteContainerPointers(contents_data_.begin(), contents_data_.end());
+ delete order_controller_;
+ NotificationService::current()->RemoveObserver(this,
+ NOTIFY_TAB_CONTENTS_DESTROYED, NotificationService::AllSources());
+}
+
+void TabStripModel::AddObserver(TabStripModelObserver* observer) {
+ observers_.AddObserver(observer);
+}
+
+void TabStripModel::RemoveObserver(TabStripModelObserver* observer) {
+ observers_.RemoveObserver(observer);
+}
+
+void TabStripModel::SetOrderController(
+ TabStripModelOrderController* order_controller) {
+ if (order_controller_)
+ delete order_controller_;
+ order_controller_ = order_controller;
+}
+
+bool TabStripModel::ContainsIndex(int index) const {
+ return index >= 0 && index < count();
+}
+
+void TabStripModel::AppendTabContents(TabContents* contents, bool foreground) {
+ // Tabs opened in the foreground using this method inherit the group of the
+ // previously selected tab.
+ InsertTabContentsAt(count(), contents, foreground, foreground);
+}
+
+void TabStripModel::InsertTabContentsAt(int index,
+ TabContents* contents,
+ bool foreground,
+ bool inherit_group) {
+ // Have to get the selected contents before we monkey with |contents_|
+ // otherwise we run into problems when we try to change the selected contents
+ // since the old contents and the new contents will be the same...
+ TabContents* selected_contents = GetSelectedTabContents();
+ TabContentsData* data = new TabContentsData(contents);
+ if (inherit_group && selected_contents) {
+ if (foreground) {
+ // Forget any existing relationships, we don't want to make things too
+ // confusing by having multiple groups active at the same time.
+ ForgetAllOpeners();
+ }
+ // Anything opened by a link we deem to have an opener.
+ data->SetGroup(selected_contents->controller());
+ }
+ contents_data_.insert(contents_data_.begin() + index, data);
+
+ FOR_EACH_OBSERVER(TabStripModelObserver, observers_,
+ TabInsertedAt(contents, index, foreground));
+
+ if (foreground)
+ ChangeSelectedContentsFrom(selected_contents, index, false);
+}
+
+void TabStripModel::ReplaceNavigationControllerAt(
+ int index, NavigationController* controller) {
+ // This appears to be OK with no flicker since no redraw event
+ // occurs between the call to add an aditional tab and one to close
+ // the previous tab.
+ InsertTabContentsAt(index + 1, controller->active_contents(), true, true);
+ InternalCloseTabContentsAt(index, false);
+}
+
+TabContents* TabStripModel::DetachTabContentsAt(int index) {
+ if (contents_data_.empty())
+ return NULL;
+
+ DCHECK(ContainsIndex(index));
+ TabContents* removed_contents = GetContentsAt(index);
+ next_selected_index_ = order_controller_->DetermineNewSelectedIndex(index);
+ delete contents_data_.at(index);
+ contents_data_.erase(contents_data_.begin() + index);
+ if (contents_data_.empty())
+ closing_all_ = true;
+ TabStripModelObservers::Iterator iter(observers_);
+ while (TabStripModelObserver* obs = iter.GetNext()) {
+ obs->TabDetachedAt(removed_contents, index);
+ if (empty())
+ obs->TabStripEmpty();
+ }
+ if (!contents_data_.empty()) {
+ if (index == selected_index_) {
+ ChangeSelectedContentsFrom(removed_contents, next_selected_index_,
+ false);
+ } else if (index < selected_index_) {
+ // If the removed tab was before the selected index, we need to account
+ // for this in the selected index...
+ SelectTabContentsAt(selected_index_ - 1, false);
+ }
+ }
+ next_selected_index_ = selected_index_;
+ return removed_contents;
+}
+
+void TabStripModel::SelectTabContentsAt(int index, bool user_gesture) {
+ DCHECK(ContainsIndex(index));
+ ChangeSelectedContentsFrom(GetSelectedTabContents(), index, user_gesture);
+}
+
+void TabStripModel::ReplaceTabContentsAt(int index,
+ TabContents* replacement_contents) {
+ DCHECK(ContainsIndex(index));
+ TabContents* old_contents = GetContentsAt(index);
+ contents_data_[index]->contents = replacement_contents;
+
+ FOR_EACH_OBSERVER(TabStripModelObserver, observers_,
+ TabChangedAt(replacement_contents, index));
+
+ // Re-use the logic for selecting tabs to ensure the replacement contents is
+ // shown and sized appropriately.
+ if (index == selected_index_) {
+ FOR_EACH_OBSERVER(TabStripModelObserver, observers_,
+ TabSelectedAt(old_contents, replacement_contents, index, false));
+ }
+}
+
+void TabStripModel::MoveTabContentsAt(int index, int to_position) {
+ DCHECK(ContainsIndex(index));
+ if (index == to_position)
+ return;
+
+ TabContentsData* moved_data = contents_data_.at(index);
+ contents_data_.erase(contents_data_.begin() + index);
+ contents_data_.insert(contents_data_.begin() + to_position, moved_data);
+
+ selected_index_ = to_position;
+
+ FOR_EACH_OBSERVER(TabStripModelObserver, observers_,
+ TabMoved(moved_data->contents, index, to_position));
+}
+
+TabContents* TabStripModel::GetSelectedTabContents() const {
+ return GetTabContentsAt(selected_index_);
+}
+
+TabContents* TabStripModel::GetTabContentsAt(int index) const {
+ if (ContainsIndex(index))
+ return GetContentsAt(index);
+ return NULL;
+}
+
+int TabStripModel::GetIndexOfTabContents(const TabContents* contents) const {
+ int index = 0;
+ TabContentsDataVector::const_iterator iter = contents_data_.begin();
+ for (; iter != contents_data_.end(); ++iter, ++index) {
+ if ((*iter)->contents == contents)
+ return index;
+ }
+ return kNoTab;
+}
+
+int TabStripModel::GetIndexOfController(
+ const NavigationController* controller) const {
+ int index = 0;
+ TabContentsDataVector::const_iterator iter = contents_data_.begin();
+ for (; iter != contents_data_.end(); ++iter, ++index) {
+ if ((*iter)->contents->controller() == controller)
+ return index;
+ }
+ return kNoTab;
+}
+
+void TabStripModel::UpdateTabContentsStateAt(int index) {
+ DCHECK(ContainsIndex(index));
+
+ FOR_EACH_OBSERVER(TabStripModelObserver, observers_,
+ TabChangedAt(GetContentsAt(index), index));
+}
+
+void TabStripModel::UpdateTabContentsLoadingAnimations() {
+ FOR_EACH_OBSERVER(TabStripModelObserver, observers_,
+ TabValidateAnimations());
+}
+
+void TabStripModel::CloseAllTabs() {
+ // Set state so that observers can adjust their behavior to suit this
+ // specific condition when CloseTabContentsAt causes a flurry of
+ // Close/Detach/Select notifications to be sent.
+ closing_all_ = true;
+ for (int i = count() - 1; i >= 0; --i)
+ CloseTabContentsAt(i);
+}
+
+bool TabStripModel::TabsAreLoading() const {
+ TabContentsDataVector::const_iterator iter = contents_data_.begin();
+ for (; iter != contents_data_.end(); ++iter) {
+ if ((*iter)->contents->is_loading())
+ return true;
+ }
+ return false;
+}
+
+bool TabStripModel::TabHasUnloadListener(int index) {
+ WebContents* web_contents = GetContentsAt(index)->AsWebContents();
+ if (web_contents) {
+ return web_contents->render_view_host()->HasUnloadListener();
+ }
+ return false;
+}
+
+NavigationController* TabStripModel::GetOpenerOfTabContentsAt(int index) {
+ DCHECK(ContainsIndex(index));
+ return contents_data_.at(index)->opener;
+}
+
+int TabStripModel::GetIndexOfNextTabContentsOpenedBy(
+ NavigationController* opener, int start_index, bool use_group) {
+ DCHECK(opener);
+ DCHECK(ContainsIndex(start_index));
+
+ TabContentsData* start_data = contents_data_.at(start_index);
+ TabContentsDataVector::const_iterator iter =
+ find(contents_data_.begin(), contents_data_.end(), start_data);
+ TabContentsDataVector::const_iterator next;
+ for (; iter != contents_data_.end(); ++iter) {
+ next = iter + 1;
+ if (next == contents_data_.end())
+ break;
+ if (OpenerMatches(*next, opener, use_group))
+ return static_cast<int>(next - contents_data_.begin());
+ }
+ iter = find(contents_data_.begin(), contents_data_.end(), start_data);
+ if (iter != contents_data_.begin()) {
+ for (--iter; iter > contents_data_.begin(); --iter) {
+ if (OpenerMatches(*iter, opener, use_group))
+ return static_cast<int>(iter - contents_data_.begin());
+ }
+ }
+ return kNoTab;
+}
+
+int TabStripModel::GetIndexOfLastTabContentsOpenedBy(
+ NavigationController* opener, int start_index) {
+ DCHECK(opener);
+ DCHECK(ContainsIndex(start_index));
+
+ TabContentsData* start_data = contents_data_.at(start_index);
+ TabContentsDataVector::const_iterator end =
+ find(contents_data_.begin(), contents_data_.end(), start_data);
+ TabContentsDataVector::const_iterator iter =
+ contents_data_.end();
+ TabContentsDataVector::const_iterator next;
+ for (; iter != end; --iter) {
+ next = iter - 1;
+ if (next == end)
+ break;
+ if ((*next)->opener == opener)
+ return static_cast<int>(next - contents_data_.begin());
+ }
+ return kNoTab;
+}
+
+void TabStripModel::ForgetAllOpeners() {
+ // Forget all opener memories so we don't do anything weird with tab
+ // re-selection ordering.
+ TabContentsDataVector::const_iterator iter = contents_data_.begin();
+ for (; iter != contents_data_.end(); ++iter)
+ (*iter)->ForgetOpener();
+}
+
+void TabStripModel::ForgetGroup(TabContents* contents) {
+ int index = GetIndexOfTabContents(contents);
+ DCHECK(ContainsIndex(index));
+ contents_data_.at(index)->SetGroup(NULL);
+}
+
+TabContents* TabStripModel::AddBlankTab(bool foreground) {
+ DCHECK(delegate_);
+ TabContents* contents = delegate_->CreateTabContentsForURL(
+ NewTabUIURL(), profile_, PageTransition::TYPED, false, NULL);
+ AddTabContents(contents, -1, PageTransition::TYPED, foreground);
+ return contents;
+}
+
+TabContents* TabStripModel::AddBlankTabAt(int index, bool foreground) {
+ DCHECK(delegate_);
+ TabContents* contents = delegate_->CreateTabContentsForURL(
+ NewTabUIURL(), profile_, PageTransition::LINK, false, NULL);
+ AddTabContents(contents, index, PageTransition::LINK, foreground);
+ return contents;
+}
+
+void TabStripModel::AddTabContents(TabContents* contents,
+ int index,
+ PageTransition::Type transition,
+ bool foreground) {
+ if (transition == PageTransition::LINK) {
+ // Only try to be clever if we're opening a LINK.
+ index = order_controller_->DetermineInsertionIndex(
+ contents, transition, foreground);
+ } else {
+ // For all other types, respect what was passed to us, normalizing -1s.
+ if (index < 0)
+ index = count();
+ }
+ TabContents* last_selected_contents = GetSelectedTabContents();
+ InsertTabContentsAt(
+ index, contents, foreground, transition == PageTransition::LINK);
+}
+
+void TabStripModel::CloseSelectedTab() {
+ CloseTabContentsAt(selected_index_);
+}
+
+void TabStripModel::SelectNextTab() {
+ // This may happen during automated testing or if a user somehow buffers
+ // many key accelerators.
+ if (empty())
+ return;
+
+ int next_index = (selected_index_ + 1) % count();
+ SelectTabContentsAt(next_index, true);
+}
+
+void TabStripModel::SelectPreviousTab() {
+ int prev_index = selected_index_ - 1;
+ if (prev_index < 0)
+ prev_index = count() + prev_index;
+ SelectTabContentsAt(prev_index, true);
+}
+
+void TabStripModel::SelectLastTab() {
+ SelectTabContentsAt(count() - 1, true);
+}
+
+void TabStripModel::TearOffTabContents(TabContents* detached_contents,
+ const gfx::Point& drop_point) {
+ DCHECK(detached_contents);
+ delegate_->CreateNewStripWithContents(detached_contents, drop_point);
+}
+
+// Context menu functions.
+bool TabStripModel::IsContextMenuCommandEnabled(
+ int context_index, ContextMenuCommand command_id) {
+ DCHECK(command_id > CommandFirst && command_id < CommandLast);
+ switch (command_id) {
+ case CommandNewTab:
+ case CommandReload:
+ case CommandCloseTab:
+ return true;
+ case CommandCloseOtherTabs:
+ return count() > 1;
+ case CommandCloseTabsToRight:
+ return context_index < (count() - 1);
+ case CommandCloseTabsOpenedBy: {
+ NavigationController* opener =
+ GetTabContentsAt(context_index)->controller();
+ int next_index =
+ GetIndexOfNextTabContentsOpenedBy(opener, context_index, true);
+ return next_index != kNoTab;
+ }
+ case CommandDuplicate:
+ if (delegate_)
+ return delegate_->CanDuplicateContentsAt(context_index);
+ else
+ return false;
+ default:
+ NOTREACHED();
+ }
+ return false;
+}
+
+void TabStripModel::ExecuteContextMenuCommand(
+ int context_index, ContextMenuCommand command_id) {
+ DCHECK(command_id > CommandFirst && command_id < CommandLast);
+ switch (command_id) {
+ case CommandNewTab:
+ UserMetrics::RecordAction(L"TabContextMenu_NewTab", profile_);
+ AddBlankTabAt(context_index + 1, true);
+ break;
+ case CommandReload:
+ UserMetrics::RecordAction(L"TabContextMenu_Reload", profile_);
+ GetContentsAt(context_index)->controller()->Reload();
+ break;
+ case CommandDuplicate:
+ if (delegate_) {
+ UserMetrics::RecordAction(L"TabContextMenu_Duplicate", profile_);
+ delegate_->DuplicateContentsAt(context_index);
+ }
+ break;
+ case CommandCloseTab:
+ UserMetrics::RecordAction(L"TabContextMenu_CloseTab", profile_);
+ CloseTabContentsAt(context_index);
+ break;
+ case CommandCloseOtherTabs: {
+ UserMetrics::RecordAction(L"TabContextMenu_CloseOtherTabs", profile_);
+ // Remove tabs before the tab to keep.
+ for (int i = 0; i < context_index; i++)
+ CloseTabContentsAt(0);
+ // Remove all tabs after the tab to keep.
+ for (int i = 1, c = count(); i < c; i++)
+ CloseTabContentsAt(1);
+ break;
+ }
+ case CommandCloseTabsToRight: {
+ UserMetrics::RecordAction(L"TabContextMenu_CloseTabsToRight", profile_);
+ for (int i = count() - 1; i > context_index; --i)
+ CloseTabContentsAt(i);
+ break;
+ }
+ case CommandCloseTabsOpenedBy: {
+ UserMetrics::RecordAction(L"TabContextMenu_CloseTabsOpenedBy", profile_);
+ NavigationController* opener =
+ GetTabContentsAt(context_index)->controller();
+ int next_index = context_index;
+ while (true) {
+ next_index = GetIndexOfNextTabContentsOpenedBy(opener, 0, true);
+ if (next_index == kNoTab)
+ break;
+ CloseTabContentsAt(next_index);
+ }
+ break;
+ }
+ default:
+ NOTREACHED();
+ }
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// TabStripModel, NotificationObserver implementation:
+
+void TabStripModel::Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details) {
+ DCHECK(type == NOTIFY_TAB_CONTENTS_DESTROYED);
+ // Sometimes, on qemu, it seems like a TabContents object can be destroyed
+ // while we still have a reference to it. We need to break this reference
+ // here so we don't crash later.
+ int index = GetIndexOfTabContents(Source<TabContents>(source).ptr());
+ if (index != TabStripModel::kNoTab) {
+ // Note that we only detach the contents here, not close it - it's already
+ // been closed. We just want to undo our bookkeeping.
+ DetachTabContentsAt(index);
+ }
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// TabStripModel, private:
+
+void TabStripModel::InternalCloseTabContentsAt(int index,
+ bool create_historical_tab) {
+ TabContents* detached_contents = GetContentsAt(index);
+
+ if (TabHasUnloadListener(index)) {
+ // If the page has unload listeners, then we tell the renderer to fire
+ // them. Once they have fired, we'll get a message back saying whether
+ // to proceed closing the page or not, which sends us back to this method
+ // with the HasUnloadListener bit cleared.
+ WebContents* web_contents = GetContentsAt(index)->AsWebContents();
+ // If we hit this code path, the tab had better be a WebContents tab.
+ DCHECK(web_contents);
+ web_contents->render_view_host()->AttemptToClosePage(false);
+ return;
+ }
+
+ // TODO: Now that we know the tab has no unload/beforeunload listeners,
+ // we should be able to do a fast shutdown of the RenderViewProcess.
+ // Make sure that we actually do.
+
+ FOR_EACH_OBSERVER(TabStripModelObserver, observers_,
+ TabClosingAt(detached_contents, index));
+
+ const bool add_to_restore_service =
+ (detached_contents && create_historical_tab &&
+ ShouldAddToTabRestoreService(detached_contents));
+ if (detached_contents) {
+ if (add_to_restore_service) {
+ profile()->GetTabRestoreService()->
+ CreateHistoricalTab(detached_contents->controller());
+ }
+ detached_contents->CloseContents();
+ // Closing the TabContents will later call back to us via
+ // NotificationObserver and detach it.
+ }
+}
+
+TabContents* TabStripModel::GetContentsAt(int index) const {
+ CHECK(ContainsIndex(index)) <<
+ "Failed to find: " << index << " in: " << count() << " entries.";
+ return contents_data_.at(index)->contents;
+}
+
+void TabStripModel::ChangeSelectedContentsFrom(
+ TabContents* old_contents, int to_index, bool user_gesture) {
+ DCHECK(ContainsIndex(to_index));
+ TabContents* new_contents = GetContentsAt(to_index);
+ if (old_contents == new_contents)
+ return;
+ TabContents* last_selected_contents = old_contents;
+ int from_index = selected_index_;
+ selected_index_ = to_index;
+
+ FOR_EACH_OBSERVER(TabStripModelObserver, observers_,
+ TabSelectedAt(last_selected_contents, new_contents, selected_index_,
+ user_gesture));
+}
+
+void TabStripModel::SetOpenerForContents(TabContents* contents,
+ TabContents* opener) {
+ int index = GetIndexOfTabContents(contents);
+ contents_data_.at(index)->opener = opener->controller();
+}
+
+bool TabStripModel::ShouldAddToTabRestoreService(TabContents* contents) {
+ if (!profile() || profile()->IsOffTheRecord() ||
+ !profile()->GetTabRestoreService()) {
+ return false;
+ }
+
+ Browser* browser =
+ Browser::GetBrowserForController(contents->controller(), NULL);
+ if (!browser)
+ return false; // Browser is null during unit tests.
+ return browser->GetType() == BrowserType::TABBED_BROWSER;
+}
+
+// static
+bool TabStripModel::OpenerMatches(TabContentsData* data,
+ NavigationController* opener,
+ bool use_group) {
+ return data->opener == opener || (use_group && data->group == opener);
+}
+
diff --git a/chrome/browser/tabs/tab_strip_model.h b/chrome/browser/tabs/tab_strip_model.h
new file mode 100644
index 0000000..aaa3edb
--- /dev/null
+++ b/chrome/browser/tabs/tab_strip_model.h
@@ -0,0 +1,536 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef CHROME_BROWSER_TABS_TAB_STRIP_MODEL_H__
+#define CHROME_BROWSER_TABS_TAB_STRIP_MODEL_H__
+
+#include <vector>
+
+#include "base/basictypes.h"
+#include "base/observer_list.h"
+#include "chrome/browser/history/history.h"
+#include "chrome/browser/site_instance.h"
+#include "chrome/common/notification_service.h"
+#include "chrome/common/page_transition_types.h"
+#include "chrome/common/pref_member.h"
+
+namespace gfx {
+class Point;
+}
+class GURL;
+class NavigationController;
+class Profile;
+class TabContents;
+class TabStripModelOrderController;
+class TabStripModel;
+
+////////////////////////////////////////////////////////////////////////////////
+//
+// TabStripModelObserver
+//
+// Objects implement this interface when they wish to be notified of changes
+// to the TabStripModel.
+//
+// Two major implementers are the TabStrip, which uses notifications sent
+// via this interface to update the presentation of the strip, and the Browser
+// object, which updates bookkeeping and shows/hides individual TabContentses.
+//
+// Register your TabStripModelObserver with the TabStripModel using its
+// Add/RemoveObserver methods.
+//
+////////////////////////////////////////////////////////////////////////////////
+class TabStripModelObserver {
+ public:
+ // A new TabContents was inserted into the TabStripModel at the specified
+ // index. |foreground| is whether or not it was opened in the foreground
+ // (selected).
+ virtual void TabInsertedAt(TabContents* contents,
+ int index,
+ bool foreground) { }
+ // The specified TabContents at |index| is being closed (and eventually
+ // destroyed).
+ virtual void TabClosingAt(TabContents* contents, int index) { }
+ // The specified TabContents at |index| is being detached, perhaps to be
+ // inserted in another TabStripModel. The implementer should take whatever
+ // action is necessary to deal with the TabContents no longer being present.
+ virtual void TabDetachedAt(TabContents* contents, int index) { }
+ // The selected TabContents changed from |old_contents| to |new_contents| at
+ // |index|. |user_gesture| specifies whether or not this was done by a user
+ // input event (e.g. clicking on a tab, keystroke) or as a side-effect of
+ // some other function.
+ virtual void TabSelectedAt(TabContents* old_contents,
+ TabContents* new_contents,
+ int index,
+ bool user_gesture) { }
+ // The specified TabContents at |from_index| was moved to |to_index|.
+ virtual void TabMoved(TabContents* contents,
+ int from_index,
+ int to_index) { }
+ // The specified TabContents at |index| changed in some way.
+ virtual void TabChangedAt(TabContents* contents, int index) { }
+ // Loading progress representations for tabs should be validated/updated.
+ virtual void TabValidateAnimations() { }
+ // 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...
+ virtual void TabStripEmpty() { }
+};
+
+///////////////////////////////////////////////////////////////////////////////
+//
+// TabStripModelDelegate
+//
+// A delegate interface that the TabStripModel uses to perform work that it
+// can't do itself, such as obtain a container HWND for creating new
+// TabContents, creating new TabStripModels for detached tabs, etc.
+//
+// This interface is typically implemented by the controller that instantiates
+// the TabStripModel (in our case the Browser object).
+//
+///////////////////////////////////////////////////////////////////////////////
+class TabStripModelDelegate {
+ public:
+ // Ask for a new TabStripModel to be created and the given tab contents to
+ // be added to it. Its presentation (e.g. a browser window) anchored at the
+ // specified creation point. It is left up to the delegate to decide how to
+ // size the window. ass an empty point (0, 0) to allow the delegate to decide
+ // where to position the window.
+ virtual void CreateNewStripWithContents(TabContents* contents,
+ const gfx::Point& creation_point) = 0;
+
+ enum {
+ TAB_MOVE_ACTION = 1,
+ TAB_TEAROFF_ACTION = 2
+ };
+
+ // Determine what drag actions are possible for the specified strip.
+ virtual int GetDragActions() const = 0;
+
+ // Creates an appropriate TabContents for the given URL. This is handled by
+ // the delegate since the TabContents may require special circumstances to
+ // exist for it to be constructed (e.g. a parent HWND).
+ // If |defer_load| is true, the navigation controller doesn't load the url.
+ // If |instance| is not null, its process is used to render the tab.
+ virtual TabContents* CreateTabContentsForURL(
+ const GURL& url,
+ Profile* profile,
+ PageTransition::Type transition,
+ bool defer_load,
+ SiteInstance* instance) const = 0;
+
+ // Show the web application context menu at the provided point. |p| is in
+ // screen coordinate system.
+ virtual void ShowApplicationMenu(const gfx::Point p) = 0;
+
+ // Return whether some contents can be duplicated.
+ virtual bool CanDuplicateContentsAt(int index) = 0;
+
+ // Duplicate the contents at the provided index and places it into its own
+ // window.
+ virtual void DuplicateContentsAt(int index) = 0;
+
+ // Called every time the the throbber needs to be updated. We have this to
+ // give the browser/frame a chance to implement some loading animation. This
+ // is used by simple web application frames.
+ virtual void ValidateLoadingAnimations() = 0;
+
+ // Called when a drag session has completed and the frame that initiated the
+ // the session should be closed.
+ virtual void CloseFrameAfterDragSession() = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+//
+// TabStripModel
+//
+// A model & low level controller of a Browser Window tabstrip. Holds a vector
+// of TabContents, and provides an API for adding, removing and shuffling
+// them, as well as a higher level API for doing specific Browser-related
+// tasks like adding new Tabs from just a URL, etc.
+//
+// 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.
+//
+// A TabStripModel also has N observers (see TabStripModelObserver above),
+// which can be registered via Add/RemoveObserver. An Observer is notified of
+// tab creations, removals, moves, and other interesting events. The
+// TabStrip implements this interface to know when to create new tabs in
+// the View, and the Browser object likewise implements to be able to update
+// its bookkeeping when such events happen.
+//
+////////////////////////////////////////////////////////////////////////////////
+class TabStripModel : public NotificationObserver {
+ public:
+ // Construct a TabStripModel with a delegate to help it do certain things
+ // (See TabStripModelDelegate documentation).
+ TabStripModel(TabStripModelDelegate* delegate, Profile* profile);
+ virtual ~TabStripModel();
+
+ // Retrieves the TabStripModelDelegate associated with this TabStripModel.
+ TabStripModelDelegate* delegate() const { return delegate_; }
+
+ // Add and remove observers to changes within this TabStripModel.
+ void AddObserver(TabStripModelObserver* observer);
+ void RemoveObserver(TabStripModelObserver* observer);
+
+ // Retrieve the number of TabContentses/emptiness of the TabStripModel.
+ int count() const { return static_cast<int>(contents_data_.size()); }
+ bool empty() const { return contents_data_.empty(); }
+
+ // Retrieve the Profile associated with this TabStripModel.
+ Profile* profile() const { return profile_; }
+
+ // Retrieve/set the active TabStripModelOrderController associated with this
+ // TabStripModel
+ TabStripModelOrderController* order_controller() const {
+ return order_controller_;
+ }
+ void SetOrderController(TabStripModelOrderController* order_controller);
+
+ // Retrieve the index of the currently selected TabContents.
+ int selected_index() const { return selected_index_; }
+
+ // See documentation for |next_selected_index_| below.
+ int next_selected_index() const { return next_selected_index_; }
+
+ // Returns true if the tabstrip is currently closing all open tabs (via a
+ // call to CloseAllTabs). As tabs close, the selection in the tabstrip
+ // changes which notifies observers, which can use this as an optimization to
+ // avoid doing meaningless or unhelpful work.
+ bool closing_all() const { return closing_all_; }
+
+ // Basic API /////////////////////////////////////////////////////////////////
+
+ static const int kNoTab = -1;
+
+ // Determines if the specified index is contained within the TabStripModel.
+ bool ContainsIndex(int index) const;
+
+ // Adds the specified TabContents in the default location. Tabs opened in the
+ // foreground inherit the group of the previously selected tab.
+ void AppendTabContents(TabContents* contents, bool foreground);
+
+ // Adds the specified TabContents in the specified location. If
+ // |inherit_group| is true, the new contents is linked to the current tab's
+ // group.
+ void InsertTabContentsAt(int index,
+ TabContents* contents,
+ bool foreground,
+ bool inherit_group);
+
+ // Closes the TabContents at the specified index. This causes the TabContents
+ // to be destroyed, but it may not happen immediately (e.g. if it's a
+ // WebContents).
+ void CloseTabContentsAt(int index) {
+ InternalCloseTabContentsAt(index, true);
+ }
+
+ // Replaces the entire state of a the tab at index by switching in a
+ // different NavigationController. This is used through the recently
+ // closed tabs list, which needs to replace a tab's current state
+ // and history with another set of contents and history.
+ //
+ // The old NavigationController is deallocated and this object takes
+ // ownership of the passed in controller.
+ void ReplaceNavigationControllerAt(int index,
+ NavigationController* controller);
+
+ // Detaches the TabContents at the specified index from this strip. The
+ // TabContents is not destroyed, just removed from display. The caller is
+ // responsible for doing something with it (e.g. stuffing it into another
+ // strip).
+ TabContents* DetachTabContentsAt(int index);
+
+ // Select the TabContents at the specified index. |user_gesture| is true if
+ // the user actually clicked on the tab or navigated to it using a keyboard
+ // command, false if the tab was selected as a by-product of some other
+ // action.
+ void SelectTabContentsAt(int index, bool user_gesture);
+
+ // Replace the TabContents at the specified index with another TabContents.
+ // This is used when a navigation causes a different TabContentsType to be
+ // required, e.g. the transition from New Tab to a web page.
+ void ReplaceTabContentsAt(int index, TabContents* replacement_contents);
+
+ // Move the TabContents at the specified index to another index. This method
+ // does NOT send Detached/Attached notifications, rather it moves the
+ // TabContents inline and sends a Moved notification instead.
+ void MoveTabContentsAt(int index, int to_position);
+
+ // Returns the currently selected TabContents, or NULL if there is none.
+ TabContents* GetSelectedTabContents() const;
+
+ // Returns the TabContents at the specified index, or NULL if there is none.
+ TabContents* GetTabContentsAt(int index) const;
+
+ // Returns the index of the specified TabContents, or -1 if the TabContents
+ // is not in this TabStripModel.
+ int GetIndexOfTabContents(const TabContents* contents) const;
+
+ // Returns the index of the specified NavigationController, or -1 if it is
+ // not in this TabStripModel.
+ int GetIndexOfController(const NavigationController* controller) const;
+
+ // Notify any observers that the TabContents at the specified index has
+ // changed in some way.
+ void UpdateTabContentsStateAt(int index);
+
+ // Notify any observers that Loading progress for TabContents should be
+ // validated.
+ // TODO(beng): (Cleanup) This should definitely be moved to the View.
+ void UpdateTabContentsLoadingAnimations();
+
+ // Make sure there is an auto-generated New Tab tab in the TabStripModel.
+ // If |force_create| is true, the New Tab will be created even if the
+ // preference is set to false (used by startup).
+ void EnsureNewTabVisible(bool force_create);
+
+ // Close all tabs at once. Code can use closing_all() above to defer
+ // operations that might otherwise by invoked by the flurry of detach/select
+ // notifications this method causes.
+ void CloseAllTabs();
+
+ // Returns true if there are any TabContents that are currently loading.
+ bool TabsAreLoading() const;
+
+ // Whether the tab has a beforeunload/unload listener that needs firing before
+ // being closed.
+ bool TabHasUnloadListener(int index);
+
+ // Returns the controller controller that opened the TabContents at |index|.
+ NavigationController* GetOpenerOfTabContentsAt(int index);
+
+ // Returns the index of the next TabContents in the sequence of TabContentses
+ // spawned by the specified NavigationController after |start_index|.
+ // If |use_group| is true, the group property of the tab is used instead of
+ // the opener to find the next tab. Under some circumstances the group
+ // relationship may exist but the opener may not.
+ int GetIndexOfNextTabContentsOpenedBy(NavigationController* opener,
+ int start_index,
+ bool use_group);
+
+ // Returns the index of the last TabContents in the model opened by the
+ // specified opener, starting at |start_index|.
+ int GetIndexOfLastTabContentsOpenedBy(NavigationController* opener,
+ int start_index);
+
+ // Forget all Opener relationships that are stored (but _not_ group
+ // relationships!) This is to reduce unpredictable tab switching behavior
+ // in complex session states. The exact circumstances under which this method
+ // is called are left up to the implementation of the selected
+ // TabStripModelOrderController.
+ void ForgetAllOpeners();
+
+ // Forgets the group affiliation of the specified TabContents. This should be
+ // called when a TabContents that is part of a logical group of tabs is
+ // moved to a new logical context by the user (e.g. by typing a new URL or
+ // selecting a bookmark).
+ void ForgetGroup(TabContents* contents);
+
+ // Command level API /////////////////////////////////////////////////////////
+
+ // Adds a blank tab to the TabStripModel.
+ TabContents* AddBlankTab(bool foreground);
+ TabContents* AddBlankTabAt(int index, bool foreground);
+
+ // Adds a TabContents at the best position in the TabStripModel given the
+ // specified insertion index, transition, etc. Ultimately, the insertion
+ // index of the TabContents is left up to the Order Controller associated
+ // with this TabStripModel, so the final insertion index may differ from
+ // |index|.
+ void AddTabContents(TabContents* contents,
+ int index,
+ PageTransition::Type transition,
+ bool foreground);
+
+ // Closes the selected TabContents.
+ void CloseSelectedTab();
+
+ // Select adjacent tabs
+ void SelectNextTab();
+ void SelectPreviousTab();
+
+ // Selects the last tab in the tab strip.
+ void SelectLastTab();
+
+ // View API //////////////////////////////////////////////////////////////////
+
+ // The specified contents should be opened in a new tabstrip.
+ void TearOffTabContents(TabContents* detached_contents,
+ const gfx::Point& drop_point);
+
+ // Context menu functions.
+ enum ContextMenuCommand {
+ CommandFirst = 0,
+ CommandNewTab,
+ CommandReload,
+ CommandDuplicate,
+ CommandCloseTab,
+ CommandCloseOtherTabs,
+ CommandCloseTabsToRight,
+ CommandCloseTabsOpenedBy,
+ CommandLast
+ };
+
+ // Returns true if the specified command is enabled.
+ bool IsContextMenuCommandEnabled(int context_index,
+ ContextMenuCommand command_id);
+
+ // Performs the action associated with the specified command for the given
+ // TabStripModel index |context_index|.
+ void ExecuteContextMenuCommand(int context_index,
+ ContextMenuCommand command_id);
+
+ // Overridden from notificationObserver:
+ virtual void Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details);
+
+ private:
+ // We cannot be constructed without a delegate.
+ TabStripModel();
+
+ // Closes the TabContents at the specified index. This causes the TabContents
+ // to be destroyed, but it may not happen immediately (e.g. if it's a
+ // WebContents). If the page in question has an unload event the TabContents
+ // will not be destroyed until after the event has completed, which will then
+ // call back into this method.
+ //
+ // The boolean parameter create_historical_tab controls whether to
+ // record this tab and its history for reopening recently closed
+ // tabs.
+ void InternalCloseTabContentsAt(int index, bool create_historical_tab);
+
+ TabContents* 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(
+ TabContents* old_contents, int to_index, bool user_gesture);
+
+ // Returns the number of New Tab tabs in the TabStripModel.
+ int GetNewTabCount() const;
+
+ // Convenience for setting the opener pointer for the specified |contents| to
+ // be |opener|'s NavigationController.
+ void SetOpenerForContents(TabContents* contents, TabContents* opener);
+
+ // Returns true if closing the tab should add it to TabRestoreService. This
+ // returns true only if the profile has a TabRestoreService and the browser
+ // type is TABBED_BROWSER.
+ bool ShouldAddToTabRestoreService(TabContents* contents);
+
+ // 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.
+ struct TabContentsData;
+ static bool OpenerMatches(TabContentsData* data,
+ NavigationController* opener,
+ bool use_group);
+
+ // Our delegate.
+ TabStripModelDelegate* delegate_;
+
+ // A hunk of data representing a TabContents and (optionally) the
+ // NavigationController that spawned it. This memory only sticks around while
+ // the TabContents is in the current TabStripModel, unless otherwise
+ // specified in code.
+ struct TabContentsData {
+ TabContents* contents;
+ // We use NavigationControllers here since they more closely model the
+ // "identity" of a Tab, TabContents can change depending on the URL loaded
+ // in the Tab.
+ // The group is used to model a set of tabs spawned from a single parent
+ // tab. This value is preserved for a given tab as long as the tab remains
+ // navigated to the link it was initially opened at or some navigation from
+ // that page (i.e. if the user types or visits a bookmark or some other
+ // navigation within that tab, the group relationship is lost). This
+ // property can safely be used to implement features that depend on a
+ // logical group of related tabs.
+ NavigationController* group;
+ // The owner models the same relationship as group, except it is more
+ // easily discarded, e.g. when the user switches to a tab not part of the
+ // same group. This property is used to determine what tab to select next
+ // when one is closed.
+ NavigationController* opener;
+ explicit TabContentsData(TabContents* a_contents)
+ : contents(a_contents) {
+ 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;
+ }
+ };
+
+ // The TabContents data currently hosted within this TabStripModel.
+ typedef std::vector<TabContentsData*> TabContentsDataVector;
+ TabContentsDataVector contents_data_;
+
+ // The index of the TabContents in |contents_| that is currently selected.
+ int selected_index_;
+
+ // The index of the TabContnets in |contents_| that will be selected when the
+ // current composite operation completes. A Tab Detach is an example of a
+ // composite operation - it not only removes a tab from the strip, but also
+ // causes the selection to shift. Some code needs to know what the next
+ // selected index will be. In other cases, this value is equal to
+ // selected_index_.
+ int next_selected_index_;
+
+ // A profile associated with this TabStripModel, used when creating new Tabs.
+ Profile* profile_;
+
+ // True if all tabs are currently being closed via CloseAllTabs.
+ bool closing_all_;
+
+ // An object that determines where new Tabs should be inserted and where
+ // selection should move when a Tab is closed.
+ TabStripModelOrderController* order_controller_;
+
+ // Our observers.
+ typedef ObserverList<TabStripModelObserver> TabStripModelObservers;
+ TabStripModelObservers observers_;
+
+ DISALLOW_EVIL_CONSTRUCTORS(TabStripModel);
+};
+
+#endif // CHROME_BROWSER_TABS_TAB_STRIP_MODEL_H__
diff --git a/chrome/browser/tabs/tab_strip_model_order_controller.cc b/chrome/browser/tabs/tab_strip_model_order_controller.cc
new file mode 100644
index 0000000..7f96399
--- /dev/null
+++ b/chrome/browser/tabs/tab_strip_model_order_controller.cc
@@ -0,0 +1,146 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "chrome/browser/tabs/tab_strip_model_order_controller.h"
+
+#include "chrome/browser/tab_contents.h"
+#include "chrome/browser/tabs/tab_strip_model.h"
+#include "chrome/common/pref_names.h"
+
+///////////////////////////////////////////////////////////////////////////////
+// TabStripModelOrderController, public:
+
+TabStripModelOrderController::TabStripModelOrderController(
+ TabStripModel* tabstrip) : tabstrip_(tabstrip) {
+ tabstrip_->AddObserver(this);
+}
+
+TabStripModelOrderController::~TabStripModelOrderController() {
+ tabstrip_->RemoveObserver(this);
+}
+
+int TabStripModelOrderController::DetermineInsertionIndex(
+ TabContents* new_contents,
+ PageTransition::Type transition,
+ bool foreground) {
+ int tab_count = tabstrip_->count();
+ if (!tab_count)
+ return 0;
+
+ if (transition == PageTransition::LINK && tabstrip_->selected_index() != -1) {
+ if (foreground) {
+ // If the page was opened in the foreground by a link click in another 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;
+ }
+ NavigationController* opener =
+ tabstrip_->GetSelectedTabContents()->controller();
+ // Get the index of the next item opened by this tab, and insert before
+ // it...
+ int index = tabstrip_->GetIndexOfLastTabContentsOpenedBy(
+ opener, tabstrip_->selected_index());
+ if (index != TabStripModel::kNoTab)
+ return index + 1;
+ // Otherwise insert adjacent to opener...
+ return tabstrip_->selected_index() + 1;
+ }
+ // In other cases, such as Ctrl+T, open at the end of the strip.
+ return tab_count;
+}
+
+int TabStripModelOrderController::DetermineNewSelectedIndex(
+ int removing_index) const {
+ int tab_count = tabstrip_->count();
+ DCHECK(removing_index >= 0 && removing_index < tab_count);
+ NavigationController* parent_opener =
+ tabstrip_->GetOpenerOfTabContentsAt(removing_index);
+ // First see if the index being removed has any "child" tabs. If it does, we
+ // want to select the first in that child group, not the next tab in the same
+ // group of the removed tab.
+ NavigationController* removed_controller =
+ tabstrip_->GetTabContentsAt(removing_index)->controller();
+ int index = tabstrip_->GetIndexOfNextTabContentsOpenedBy(removed_controller,
+ removing_index,
+ false);
+ if (index != TabStripModel::kNoTab)
+ return GetValidIndex(index, removing_index);
+
+ if (parent_opener) {
+ // If the tab was in a group, shift selection to the next tab in the group.
+ int index = tabstrip_->GetIndexOfNextTabContentsOpenedBy(parent_opener,
+ removing_index,
+ false);
+ if (index != TabStripModel::kNoTab)
+ return GetValidIndex(index, removing_index);
+
+ // If we can't find a subsequent group member, just fall back to the
+ // parent_opener itself. Note that we use "group" here since opener is
+ // reset by select operations..
+ index = tabstrip_->GetIndexOfController(parent_opener);
+ if (index != TabStripModel::kNoTab)
+ return GetValidIndex(index, removing_index);
+ }
+
+ // No opener set, fall through to the default handler...
+ int selected_index = tabstrip_->selected_index();
+ if (selected_index >= (tab_count - 1))
+ return selected_index - 1;
+ return selected_index;
+}
+
+void TabStripModelOrderController::TabSelectedAt(TabContents* old_contents,
+ TabContents* new_contents,
+ int index,
+ bool user_gesture) {
+ NavigationController* old_opener = NULL;
+ if (old_contents) {
+ int index = tabstrip_->GetIndexOfTabContents(old_contents);
+ if (index != TabStripModel::kNoTab)
+ old_opener = tabstrip_->GetOpenerOfTabContentsAt(index);
+ }
+ NavigationController* new_opener =
+ tabstrip_->GetOpenerOfTabContentsAt(index);
+ if (user_gesture && new_opener != old_opener &&
+ new_opener != old_contents->controller() &&
+ old_opener != new_contents->controller()) {
+ tabstrip_->ForgetAllOpeners();
+ }
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// TabStripModelOrderController, private:
+
+int TabStripModelOrderController::GetValidIndex(int index,
+ int removing_index) const {
+ if (removing_index < index)
+ index = std::max(0, index - 1);
+ return index;
+}
diff --git a/chrome/browser/tabs/tab_strip_model_order_controller.h b/chrome/browser/tabs/tab_strip_model_order_controller.h
new file mode 100644
index 0000000..9a5f86d
--- /dev/null
+++ b/chrome/browser/tabs/tab_strip_model_order_controller.h
@@ -0,0 +1,75 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef CHROME_BROWSER_TABS_TAB_STRIP_MODEL_ORDER_CONTROLLER_H__
+#define CHROME_BROWSER_TABS_TAB_STRIP_MODEL_ORDER_CONTROLLER_H__
+
+#include "chrome/browser/tabs/tab_strip_model.h"
+#include "chrome/common/page_transition_types.h"
+#include "chrome/common/pref_member.h"
+
+class TabContents;
+class TabStripModel;
+
+///////////////////////////////////////////////////////////////////////////////
+// TabStripModelOrderController
+//
+// An object that allows different types of ordering and reselection to be
+// heuristics plugged into a TabStripModel.
+//
+class TabStripModelOrderController : public TabStripModelObserver {
+ public:
+ explicit TabStripModelOrderController(TabStripModel* tabstrip);
+ virtual ~TabStripModelOrderController();
+
+ // Determine where to place a newly opened tab by using the supplied
+ // transition and foreground flag to figure out how it was opened.
+ virtual int DetermineInsertionIndex(TabContents* new_contents,
+ PageTransition::Type transition,
+ bool foreground);
+
+ // Determine where to shift selection after a tab is closed.
+ virtual int DetermineNewSelectedIndex(int removed_index) const;
+
+ // Overridden from TabStripModelObserver:
+ virtual void TabSelectedAt(TabContents* old_contents,
+ TabContents* new_contents,
+ int index,
+ bool user_gesture);
+
+ protected:
+ // Returns a valid index to be selected after the tab at |removing_index| is
+ // closed. If |index| is after |removing_index|, |index| is adjusted to
+ // reflect the fact that |removing_index| is going away.
+ int GetValidIndex(int index, int removing_index) const;
+
+ TabStripModel* tabstrip_;
+};
+
+#endif CHROME_BROWSER_TABS_TAB_STRIP_MODEL_ORDER_CONTROLLER_H__
diff --git a/chrome/browser/tabs/tab_strip_model_unittest.cc b/chrome/browser/tabs/tab_strip_model_unittest.cc
new file mode 100644
index 0000000..49e64ac
--- /dev/null
+++ b/chrome/browser/tabs/tab_strip_model_unittest.cc
@@ -0,0 +1,1130 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "base/file_util.h"
+#include "base/path_service.h"
+#include "chrome/browser/dom_ui/new_tab_ui.h"
+#include "chrome/browser/navigation_controller.h"
+#include "chrome/browser/navigation_entry.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/profile_manager.h"
+#include "chrome/browser/tabs/tab_strip_model.h"
+#include "chrome/browser/tabs/tab_strip_model_order_controller.h"
+#include "chrome/browser/tab_contents.h"
+#include "chrome/browser/tab_contents_factory.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/common/stl_util-inl.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+const TabContentsType kHTTPTabContentsType =
+ static_cast<TabContentsType>(TAB_CONTENTS_NUM_TYPES + 1);
+const TabContentsType kReplacementContentsType =
+ static_cast<TabContentsType>(kHTTPTabContentsType + 1);
+
+// Since you can't just instantiate a TabContents, and some of its methods
+// are protected, we subclass TabContents with our own testing dummy which
+// knows how to drive the base class' NavigationController as URLs are
+// loaded.
+class TabStripModelTestTabContents : public TabContents {
+ public:
+ TabStripModelTestTabContents(const TabContentsType type)
+ : TabContents(type) {
+ }
+
+ bool Navigate(const NavigationEntry& entry, bool reload) {
+ NavigationEntry* pending_entry = new NavigationEntry(entry);
+ if (pending_entry->GetPageID() == -1) {
+ pending_entry->SetPageID(g_page_id_++);
+ }
+ DidNavigateToEntry(pending_entry);
+
+ return true;
+ }
+
+ private:
+ // We need to use valid, incrementing page ids otherwise the TabContents
+ // and NavController will not play nice when we try to go back and forward.
+ static int g_page_id_;
+};
+
+int TabStripModelTestTabContents::g_page_id_ = 0;
+
+// This constructs our fake TabContents.
+class TabStripModelTestTabContentsFactory : public TabContentsFactory {
+ public:
+ virtual TabContents* CreateInstance() {
+ return new TabStripModelTestTabContents(kHTTPTabContentsType);
+ }
+
+ virtual bool CanHandleURL(const GURL& url) {
+ return url.scheme() == "http";
+ }
+};
+
+TabStripModelTestTabContentsFactory factory;
+
+class TabStripModelTest : public testing::Test {
+ public:
+ // Overridden from testing::Test
+ virtual void SetUp() {
+ TabContents::RegisterFactory(kHTTPTabContentsType, &factory);
+
+ // Name a subdirectory of the temp directory.
+ ASSERT_TRUE(PathService::Get(base::DIR_TEMP, &test_dir_));
+ file_util::AppendToPath(&test_dir_, L"TabStripModelTest");
+
+ // Create a fresh, empty copy of this directory.
+ file_util::Delete(test_dir_, true);
+ CreateDirectory(test_dir_.c_str(), NULL);
+
+ profile_path_ = test_dir_;
+ file_util::AppendToPath(&profile_path_, L"New Profile");
+
+ profile_ = ProfileManager::CreateProfile(profile_path_,
+ L"New Profile", L"new-profile", L"");
+ ASSERT_TRUE(profile_);
+ pm_.AddProfile(profile_);
+ }
+
+ virtual void TearDown() {
+ TabContents::RegisterFactory(kHTTPTabContentsType, NULL);
+
+ // Removes a profile from the set of currently-loaded profiles.
+ pm_.RemoveProfileByPath(profile_path_);
+
+ // Clean up test directory
+ ASSERT_TRUE(file_util::Delete(test_dir_, true));
+ ASSERT_FALSE(file_util::PathExists(test_dir_));
+ }
+
+ protected:
+ TabContents* CreateTabContents() {
+ TabStripModelTestTabContents* contents =
+ new TabStripModelTestTabContents(kHTTPTabContentsType);
+ contents->SetupController(profile_);
+ return contents;
+ }
+ TabContents* CreateReplacementContents() {
+ TabStripModelTestTabContents* contents =
+ new TabStripModelTestTabContents(kReplacementContentsType);
+ contents->SetupController(profile_);
+ return contents;
+ }
+
+ // Forwards a URL "load" request through to our dummy TabContents
+ // implementation.
+ void LoadURL(TabContents* contents, const std::wstring& url) {
+ contents->controller()->LoadURL(GURL(url), PageTransition::LINK);
+ }
+
+ void GoBack(TabContents* contents) {
+ contents->controller()->GoBack();
+ }
+
+ void GoForward(TabContents* contents) {
+ contents->controller()->GoForward();
+ }
+
+ void SwitchTabTo(TabContents* contents) {
+ contents->DidBecomeSelected();
+ }
+
+ Profile* profile_;
+
+ private:
+ std::wstring test_dir_;
+ std::wstring profile_path_;
+ ProfileManager pm_;
+};
+
+class MockTabStripModelObserver : public TabStripModelObserver {
+ public:
+ MockTabStripModelObserver() : empty_(true) {}
+ ~MockTabStripModelObserver() {
+ STLDeleteContainerPointers(states_.begin(), states_.end());
+ }
+
+ enum TabStripModelObserverAction {
+ INSERT,
+ CLOSE,
+ DETACH,
+ SELECT,
+ MOVE,
+ CHANGE
+ };
+
+ struct State {
+ State(TabContents* a_dst_contents,
+ int a_dst_index,
+ TabStripModelObserverAction a_action)
+ : src_contents(NULL),
+ dst_contents(a_dst_contents),
+ src_index(-1),
+ dst_index(a_dst_index),
+ action(a_action),
+ user_gesture(false),
+ foreground(false) {
+ }
+
+ TabContents* src_contents;
+ TabContents* dst_contents;
+ int src_index;
+ int dst_index;
+ bool user_gesture;
+ bool foreground;
+ TabStripModelObserverAction action;
+ };
+
+ int GetStateCount() const {
+ return static_cast<int>(states_.size());
+ }
+
+ State* GetStateAt(int index) const {
+ DCHECK(index >= 0 && index < GetStateCount());
+ return states_.at(index);
+ }
+
+ bool StateEquals(int index, const State& state) {
+ State* s = GetStateAt(index);
+ EXPECT_EQ(s->src_contents, state.src_contents);
+ EXPECT_EQ(s->dst_contents, state.dst_contents);
+ EXPECT_EQ(s->src_index, state.src_index);
+ 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->action, state.action);
+ return (s->src_contents == state.src_contents &&
+ s->dst_contents == state.dst_contents &&
+ s->src_index == state.src_index &&
+ s->dst_index == state.dst_index &&
+ s->user_gesture == state.user_gesture &&
+ s->foreground == state.foreground &&
+ s->action == state.action);
+ }
+
+ // TabStripModelObserver implementation:
+ virtual void TabInsertedAt(TabContents* contents,
+ int index,
+ bool foreground) {
+ empty_ = false;
+ State* s = new State(contents, index, INSERT);
+ s->foreground = foreground;
+ states_.push_back(s);
+ }
+ virtual void TabSelectedAt(TabContents* old_contents,
+ TabContents* new_contents,
+ int index,
+ bool user_gesture) {
+ State* s = new State(new_contents, index, SELECT);
+ s->src_contents = old_contents;
+ s->user_gesture = user_gesture;
+ states_.push_back(s);
+ }
+ virtual void TabMoved(
+ TabContents* contents, int from_index, int to_index) {
+ State* s = new State(contents, to_index, MOVE);
+ s->src_index = from_index;
+ states_.push_back(s);
+ }
+
+ virtual void TabClosingAt(TabContents* contents, int index) {
+ states_.push_back(new State(contents, index, CLOSE));
+ }
+ virtual void TabDetachedAt(TabContents* contents, int index) {
+ states_.push_back(new State(contents, index, DETACH));
+ }
+ virtual void TabChangedAt(TabContents* contents, int index) {
+ states_.push_back(new State(contents, index, CHANGE));
+ }
+ virtual void TabStripEmpty() {
+ empty_ = true;
+ }
+
+ void ClearStates() {
+ STLDeleteContainerPointers(states_.begin(), states_.end());
+ states_.clear();
+ }
+
+ bool empty() const { return empty_; }
+
+ private:
+ std::vector<State*> states_;
+
+ bool empty_;
+
+ DISALLOW_EVIL_CONSTRUCTORS(MockTabStripModelObserver);
+};
+
+TEST_F(TabStripModelTest, TestBasicAPI) {
+ TabStripModel tabstrip(NULL, profile_);
+ MockTabStripModelObserver observer;
+ tabstrip.AddObserver(&observer);
+
+ EXPECT_TRUE(tabstrip.empty());
+
+ typedef MockTabStripModelObserver::State State;
+
+ TabContents* contents1 = CreateTabContents();
+
+ // 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.
+
+ // Test AppendTabContents, ContainsIndex
+ {
+ EXPECT_FALSE(tabstrip.ContainsIndex(0));
+ tabstrip.AppendTabContents(contents1, true);
+ EXPECT_TRUE(tabstrip.ContainsIndex(0));
+ EXPECT_EQ(1, tabstrip.count());
+ EXPECT_EQ(2, observer.GetStateCount());
+ State s1(contents1, 0, MockTabStripModelObserver::INSERT);
+ s1.foreground = true;
+ EXPECT_TRUE(observer.StateEquals(0, s1));
+ State s2(contents1, 0, MockTabStripModelObserver::SELECT);
+ s2.src_contents = NULL;
+ EXPECT_TRUE(observer.StateEquals(1, s2));
+ observer.ClearStates();
+ }
+
+ // Test InsertTabContentsAt, foreground tab.
+ TabContents* contents2 = CreateTabContents();
+ {
+ tabstrip.InsertTabContentsAt(1, contents2, true, false);
+
+ EXPECT_EQ(2, tabstrip.count());
+ EXPECT_EQ(2, observer.GetStateCount());
+ State s1(contents2, 1, MockTabStripModelObserver::INSERT);
+ s1.foreground = true;
+ EXPECT_TRUE(observer.StateEquals(0, s1));
+ State s2(contents2, 1, MockTabStripModelObserver::SELECT);
+ s2.src_contents = contents1;
+ EXPECT_TRUE(observer.StateEquals(1, s2));
+ observer.ClearStates();
+ }
+
+ // Test InsertTabContentsAt, background tab.
+ TabContents* contents3 = CreateTabContents();
+ {
+ tabstrip.InsertTabContentsAt(2, contents3, false, false);
+
+ EXPECT_EQ(3, tabstrip.count());
+ EXPECT_EQ(1, observer.GetStateCount());
+ State s1(contents3, 2, MockTabStripModelObserver::INSERT);
+ s1.foreground = false;
+ EXPECT_TRUE(observer.StateEquals(0, s1));
+ observer.ClearStates();
+ }
+
+ // Test SelectTabContentsAt
+ {
+ tabstrip.SelectTabContentsAt(2, true);
+ EXPECT_EQ(1, observer.GetStateCount());
+ State s1(contents3, 2, MockTabStripModelObserver::SELECT);
+ s1.src_contents = contents2;
+ s1.user_gesture = true;
+ EXPECT_TRUE(observer.StateEquals(0, s1));
+ observer.ClearStates();
+ }
+
+ // Test ReplaceTabContentsAt, replacing the selected index
+ TabContents* replacement_contents3 = CreateReplacementContents();
+ {
+ tabstrip.ReplaceTabContentsAt(2, replacement_contents3);
+ EXPECT_EQ(2, observer.GetStateCount());
+ State s1(replacement_contents3, 2, MockTabStripModelObserver::CHANGE);
+ EXPECT_TRUE(observer.StateEquals(0, s1));
+ State s2(replacement_contents3, 2, MockTabStripModelObserver::SELECT);
+ s2.src_contents = contents3;
+ s2.user_gesture = false;
+ EXPECT_TRUE(observer.StateEquals(1, s2));
+ observer.ClearStates();
+ }
+
+ // Test ReplaceTabContentsAt, replacing NOT the selected index
+ TabContents* replacement_contents2 = CreateReplacementContents();
+ {
+ tabstrip.ReplaceTabContentsAt(1, replacement_contents2);
+
+ EXPECT_EQ(1, observer.GetStateCount());
+ State s1(replacement_contents2, 1, MockTabStripModelObserver::CHANGE);
+ EXPECT_TRUE(observer.StateEquals(0, s1));
+ observer.ClearStates();
+ }
+
+ // Test DetachTabContentsAt
+ {
+ // Detach
+ TabContents* detached = tabstrip.DetachTabContentsAt(2);
+ // ... and append again because we want this for later.
+ tabstrip.AppendTabContents(detached, true);
+ EXPECT_EQ(4, observer.GetStateCount());
+ State s1(detached, 2, MockTabStripModelObserver::DETACH);
+ EXPECT_TRUE(observer.StateEquals(0, s1));
+ State s2(replacement_contents2, 1, MockTabStripModelObserver::SELECT);
+ s2.src_contents = replacement_contents3;
+ s2.user_gesture = false;
+ EXPECT_TRUE(observer.StateEquals(1, s2));
+ State s3(detached, 2, MockTabStripModelObserver::INSERT);
+ s3.foreground = true;
+ EXPECT_TRUE(observer.StateEquals(2, s3));
+ State s4(detached, 2, MockTabStripModelObserver::SELECT);
+ s4.src_contents = replacement_contents2;
+ s4.user_gesture = false;
+ EXPECT_TRUE(observer.StateEquals(3, s4));
+ observer.ClearStates();
+ }
+
+ // Test CloseTabContentsAt
+ {
+ tabstrip.CloseTabContentsAt(2);
+ EXPECT_EQ(2, tabstrip.count());
+
+ EXPECT_EQ(3, observer.GetStateCount());
+ State s1(replacement_contents3, 2, MockTabStripModelObserver::CLOSE);
+ EXPECT_TRUE(observer.StateEquals(0, s1));
+ State s2(replacement_contents3, 2, MockTabStripModelObserver::DETACH);
+ EXPECT_TRUE(observer.StateEquals(1, s2));
+ State s3(replacement_contents2, 1, MockTabStripModelObserver::SELECT);
+ s3.src_contents = replacement_contents3;
+ s3.user_gesture = false;
+ EXPECT_TRUE(observer.StateEquals(2, s3));
+ observer.ClearStates();
+ }
+
+ // Test MoveTabContentsAt
+ {
+ tabstrip.MoveTabContentsAt(1, 0);
+
+ EXPECT_EQ(1, observer.GetStateCount());
+ State s1(replacement_contents2, 0, MockTabStripModelObserver::MOVE);
+ s1.src_index = 1;
+ EXPECT_TRUE(observer.StateEquals(0, s1));
+ observer.ClearStates();
+ }
+
+ // Test Getters
+ {
+ EXPECT_EQ(replacement_contents2, tabstrip.GetSelectedTabContents());
+ EXPECT_EQ(replacement_contents2, tabstrip.GetTabContentsAt(0));
+ EXPECT_EQ(contents1, tabstrip.GetTabContentsAt(1));
+ EXPECT_EQ(0, tabstrip.GetIndexOfTabContents(replacement_contents2));
+ EXPECT_EQ(1, tabstrip.GetIndexOfTabContents(contents1));
+ EXPECT_EQ(0, tabstrip.GetIndexOfController(
+ replacement_contents2->controller()));
+ EXPECT_EQ(1, tabstrip.GetIndexOfController(contents1->controller()));
+ }
+
+ // Test UpdateTabContentsStateAt
+ {
+ tabstrip.UpdateTabContentsStateAt(0);
+ EXPECT_EQ(1, observer.GetStateCount());
+ State s1(replacement_contents2, 0, MockTabStripModelObserver::CHANGE);
+ EXPECT_TRUE(observer.StateEquals(0, s1));
+ observer.ClearStates();
+ }
+
+ // Test SelectNextTab, SelectPreviousTab, SelectLastTab
+ {
+ // Make sure the second of the two tabs is selected first...
+ tabstrip.SelectTabContentsAt(1, true);
+ tabstrip.SelectPreviousTab();
+ EXPECT_EQ(0, tabstrip.selected_index());
+ tabstrip.SelectLastTab();
+ EXPECT_EQ(1, tabstrip.selected_index());
+ tabstrip.SelectNextTab();
+ EXPECT_EQ(0, tabstrip.selected_index());
+ }
+
+ // Test CloseSelectedTab
+ {
+ tabstrip.CloseSelectedTab();
+ // |CloseSelectedTab| calls CloseTabContentsAt, we already tested that, now
+ // just verify that the count and selected index have changed
+ // appropriately...
+ EXPECT_EQ(1, tabstrip.count());
+ EXPECT_EQ(0, tabstrip.selected_index());
+ }
+
+ tabstrip.CloseAllTabs();
+ // TabStripModel should now be empty.
+ EXPECT_TRUE(tabstrip.empty());
+
+ // Opener methods are tested below...
+
+ tabstrip.RemoveObserver(&observer);
+}
+
+TEST_F(TabStripModelTest, TestBasicOpenerAPI) {
+ TabStripModel tabstrip(NULL, profile_);
+ EXPECT_TRUE(tabstrip.empty());
+
+ // This is a basic test of opener functionality. opener_contents is created
+ // as the first tab in the strip and then we create 5 other tabs in the
+ // background with opener_contents set as their opener.
+
+ TabContents* opener_contents = CreateTabContents();
+ NavigationController* opener = opener_contents->controller();
+ tabstrip.AppendTabContents(opener_contents, true);
+ TabContents* contents1 = CreateTabContents();
+ TabContents* contents2 = CreateTabContents();
+ TabContents* contents3 = CreateTabContents();
+ TabContents* contents4 = CreateTabContents();
+ TabContents* contents5 = CreateTabContents();
+
+ // We use |InsertTabContentsAt| here instead of AppendTabContents so that
+ // openership relationships are preserved.
+ tabstrip.InsertTabContentsAt(tabstrip.count(), contents1, false, true);
+ tabstrip.InsertTabContentsAt(tabstrip.count(), contents2, false, true);
+ tabstrip.InsertTabContentsAt(tabstrip.count(), contents3, false, true);
+ tabstrip.InsertTabContentsAt(tabstrip.count(), contents4, false, true);
+ tabstrip.InsertTabContentsAt(tabstrip.count(), contents5, false, true);
+
+ // All the tabs should have the same opener.
+ for (int i = 1; i < tabstrip.count(); ++i)
+ EXPECT_EQ(opener, tabstrip.GetOpenerOfTabContentsAt(i));
+
+ // If there is a next adjacent item, then the index should be of that item.
+ EXPECT_EQ(2, tabstrip.GetIndexOfNextTabContentsOpenedBy(opener, 1, false));
+ // If the last tab in the group is closed, the preceding tab in the same
+ // group should be selected.
+ EXPECT_EQ(4, tabstrip.GetIndexOfNextTabContentsOpenedBy(opener, 5, false));
+
+ // Tests the method that finds the last tab opened by the same opener in the
+ // strip (this is the insertion index for the next background tab for the
+ // specified opener).
+ EXPECT_EQ(5, tabstrip.GetIndexOfLastTabContentsOpenedBy(opener, 1));
+
+ // For a tab that has opened no other tabs, the return value should always be
+ // -1...
+ NavigationController* o1 = contents1->controller();
+ EXPECT_EQ(-1, tabstrip.GetIndexOfNextTabContentsOpenedBy(o1, 3, false));
+ EXPECT_EQ(-1, tabstrip.GetIndexOfLastTabContentsOpenedBy(o1, 3));
+
+ // ForgetAllOpeners should destroy all opener relationships.
+ tabstrip.ForgetAllOpeners();
+ EXPECT_EQ(-1, tabstrip.GetIndexOfNextTabContentsOpenedBy(opener, 1, false));
+ EXPECT_EQ(-1, tabstrip.GetIndexOfNextTabContentsOpenedBy(opener, 5, false));
+ EXPECT_EQ(-1, tabstrip.GetIndexOfLastTabContentsOpenedBy(opener, 1));
+
+ tabstrip.CloseAllTabs();
+ EXPECT_TRUE(tabstrip.empty());
+}
+
+static int GetInsertionIndex(TabStripModel* tabstrip, TabContents* contents) {
+ return tabstrip->order_controller()->DetermineInsertionIndex(
+ contents, PageTransition::LINK, false);
+}
+
+static void InsertTabContentses(TabStripModel* tabstrip,
+ TabContents* contents1,
+ TabContents* contents2,
+ TabContents* contents3) {
+ tabstrip->InsertTabContentsAt(GetInsertionIndex(tabstrip, contents1),
+ contents1, false, true);
+ tabstrip->InsertTabContentsAt(GetInsertionIndex(tabstrip, contents2),
+ contents2, false, true);
+ tabstrip->InsertTabContentsAt(GetInsertionIndex(tabstrip, contents3),
+ contents3, false, true);
+}
+
+// Tests opening background tabs.
+TEST_F(TabStripModelTest, TestLTRInsertionOptions) {
+ TabStripModel tabstrip(NULL, profile_);
+ EXPECT_TRUE(tabstrip.empty());
+
+ TabContents* opener_contents = CreateTabContents();
+ tabstrip.AppendTabContents(opener_contents, true);
+
+ TabContents* contents1 = CreateTabContents();
+ TabContents* contents2 = CreateTabContents();
+ TabContents* contents3 = CreateTabContents();
+
+ // Test LTR
+ InsertTabContentses(&tabstrip, contents1, contents2, contents3);
+ EXPECT_EQ(contents1, tabstrip.GetTabContentsAt(1));
+ EXPECT_EQ(contents2, tabstrip.GetTabContentsAt(2));
+ EXPECT_EQ(contents3, tabstrip.GetTabContentsAt(3));
+
+ tabstrip.CloseAllTabs();
+ EXPECT_TRUE(tabstrip.empty());
+}
+
+// This test constructs a tabstrip, and then simulates loading several tabs in
+// the background from link clicks on the first tab. Then it simulates opening
+// a new tab from the first tab in the foreground via a link click, verifies
+// that this tab is opened adjacent to the opener, then closes it.
+// Finally it tests that a tab opened for some non-link purpose openes at the
+// end of the strip, not bundled to any existing context.
+TEST_F(TabStripModelTest, TestInsertionIndexDetermination) {
+ TabStripModel tabstrip(NULL, profile_);
+ EXPECT_TRUE(tabstrip.empty());
+
+ TabContents* opener_contents = CreateTabContents();
+ NavigationController* opener = opener_contents->controller();
+ tabstrip.AppendTabContents(opener_contents, true);
+
+ // Open some other random unrelated tab in the background to monkey with our
+ // insertion index.
+ TabContents* other_contents = CreateTabContents();
+ tabstrip.AppendTabContents(other_contents, false);
+
+ TabContents* contents1 = CreateTabContents();
+ TabContents* contents2 = CreateTabContents();
+ TabContents* contents3 = CreateTabContents();
+
+ // Start by testing LTR
+ InsertTabContentses(&tabstrip, contents1, contents2, contents3);
+ EXPECT_EQ(opener_contents, tabstrip.GetTabContentsAt(0));
+ EXPECT_EQ(contents1, tabstrip.GetTabContentsAt(1));
+ EXPECT_EQ(contents2, tabstrip.GetTabContentsAt(2));
+ EXPECT_EQ(contents3, tabstrip.GetTabContentsAt(3));
+ EXPECT_EQ(other_contents, tabstrip.GetTabContentsAt(4));
+
+ // The opener API should work...
+ EXPECT_EQ(3, tabstrip.GetIndexOfNextTabContentsOpenedBy(opener, 2, false));
+ EXPECT_EQ(2, tabstrip.GetIndexOfNextTabContentsOpenedBy(opener, 3, false));
+ EXPECT_EQ(3, tabstrip.GetIndexOfLastTabContentsOpenedBy(opener, 1));
+
+ // Now open a foreground tab from a link. It should be opened adjacent to the
+ // opener tab.
+ TabContents* fg_link_contents = CreateTabContents();
+ int insert_index = tabstrip.order_controller()->DetermineInsertionIndex(
+ fg_link_contents, PageTransition::LINK, true);
+ EXPECT_EQ(1, insert_index);
+ tabstrip.InsertTabContentsAt(insert_index, fg_link_contents, true, true);
+ EXPECT_EQ(1, tabstrip.selected_index());
+ EXPECT_EQ(fg_link_contents, tabstrip.GetSelectedTabContents());
+
+ // Now close this contents. The selection should move to the opener contents.
+ tabstrip.CloseSelectedTab();
+ EXPECT_EQ(0, tabstrip.selected_index());
+
+ // Now open a new empty tab. It should open at the end of the strip.
+ TabContents* fg_nonlink_contents = CreateTabContents();
+ insert_index = tabstrip.order_controller()->DetermineInsertionIndex(
+ fg_nonlink_contents, PageTransition::AUTO_BOOKMARK, true);
+ EXPECT_EQ(tabstrip.count(), insert_index);
+ // We break the opener relationship...
+ tabstrip.InsertTabContentsAt(insert_index, fg_nonlink_contents, false, false);
+ // Now select it, so that user_gesture == true causes the opener relationship
+ // to be forgotten...
+ tabstrip.SelectTabContentsAt(tabstrip.count() - 1, true);
+ EXPECT_EQ(tabstrip.count() - 1, tabstrip.selected_index());
+ EXPECT_EQ(fg_nonlink_contents, tabstrip.GetSelectedTabContents());
+
+ // Verify that all opener relationships are forgotten.
+ EXPECT_EQ(-1, tabstrip.GetIndexOfNextTabContentsOpenedBy(opener, 2, false));
+ EXPECT_EQ(-1, tabstrip.GetIndexOfNextTabContentsOpenedBy(opener, 3, false));
+ EXPECT_EQ(-1, tabstrip.GetIndexOfNextTabContentsOpenedBy(opener, 3, false));
+ EXPECT_EQ(-1, tabstrip.GetIndexOfLastTabContentsOpenedBy(opener, 1));
+
+ tabstrip.CloseAllTabs();
+ EXPECT_TRUE(tabstrip.empty());
+}
+
+// Tests that selection is shifted to the correct tab when a tab is closed.
+// If a tab is in the background when it is closed, the selection does not
+// change.
+// If a tab is in the foreground (selected),
+// If that tab does not have an opener, selection shifts to the right.
+// If the tab has an opener,
+// The next tab (scanning LTR) in the entire strip that has the same opener
+// is selected
+// If there are no other tabs that have the same opener,
+// The opener is selected
+//
+TEST_F(TabStripModelTest, TestSelectOnClose) {
+ TabStripModel tabstrip(NULL, profile_);
+ EXPECT_TRUE(tabstrip.empty());
+
+ TabContents* opener_contents = CreateTabContents();
+ NavigationController* opener = opener_contents->controller();
+ tabstrip.AppendTabContents(opener_contents, true);
+
+ TabContents* contents1 = CreateTabContents();
+ TabContents* contents2 = CreateTabContents();
+ TabContents* contents3 = CreateTabContents();
+
+ // Note that we use Detach instead of Close throughout this test to avoid
+ // having to keep reconstructing these TabContentses.
+
+ // First test that closing tabs that are in the background doesn't adjust the
+ // current selection.
+ InsertTabContentses(&tabstrip, contents1, contents2, contents3);
+ EXPECT_EQ(0, tabstrip.selected_index());
+
+ tabstrip.DetachTabContentsAt(1);
+ EXPECT_EQ(0, tabstrip.selected_index());
+
+ for (int i = tabstrip.count() - 1; i >= 1; --i)
+ tabstrip.DetachTabContentsAt(i);
+
+ // Now test that when a tab doesn't have an opener, selection shifts to the
+ // right when the tab is closed.
+ InsertTabContentses(&tabstrip, contents1, contents2, contents3);
+ EXPECT_EQ(0, tabstrip.selected_index());
+
+ tabstrip.ForgetAllOpeners();
+ tabstrip.SelectTabContentsAt(1, true);
+ EXPECT_EQ(1, tabstrip.selected_index());
+ tabstrip.DetachTabContentsAt(1);
+ EXPECT_EQ(1, tabstrip.selected_index());
+ tabstrip.DetachTabContentsAt(1);
+ EXPECT_EQ(1, tabstrip.selected_index());
+ tabstrip.DetachTabContentsAt(1);
+ EXPECT_EQ(0, tabstrip.selected_index());
+
+ for (int i = tabstrip.count() - 1; i >= 1; --i)
+ tabstrip.DetachTabContentsAt(i);
+
+ // Now test that when a tab does have an opener, it selects the next tab
+ // opened by the same opener scanning LTR when it is closed.
+ InsertTabContentses(&tabstrip, contents1, contents2, contents3);
+ EXPECT_EQ(0, tabstrip.selected_index());
+ tabstrip.SelectTabContentsAt(2, false);
+ EXPECT_EQ(2, tabstrip.selected_index());
+ tabstrip.CloseTabContentsAt(2);
+ EXPECT_EQ(2, tabstrip.selected_index());
+ tabstrip.CloseTabContentsAt(2);
+ EXPECT_EQ(1, tabstrip.selected_index());
+ tabstrip.CloseTabContentsAt(1);
+ EXPECT_EQ(0, tabstrip.selected_index());
+
+ // Finally test that when a tab has no "siblings" that the opener is
+ // selected.
+ TabContents* other_contents = CreateTabContents();
+ tabstrip.InsertTabContentsAt(1, other_contents, false, false);
+ EXPECT_EQ(2, tabstrip.count());
+ TabContents* opened_contents = CreateTabContents();
+ tabstrip.InsertTabContentsAt(2, opened_contents, true, true);
+ EXPECT_EQ(2, tabstrip.selected_index());
+ tabstrip.CloseTabContentsAt(2);
+ EXPECT_EQ(0, tabstrip.selected_index());
+
+ tabstrip.CloseAllTabs();
+ EXPECT_TRUE(tabstrip.empty());
+}
+
+// Tests the following context menu commands:
+// - Close Tab
+// - Close Other Tabs
+// - Close Tabs To Right
+// - Close Tabs Opened By
+TEST_F(TabStripModelTest, TestContextMenuCloseCommands) {
+ TabStripModel tabstrip(NULL, profile_);
+ EXPECT_TRUE(tabstrip.empty());
+
+ TabContents* opener_contents = CreateTabContents();
+ NavigationController* opener = opener_contents->controller();
+ tabstrip.AppendTabContents(opener_contents, true);
+
+ TabContents* contents1 = CreateTabContents();
+ TabContents* contents2 = CreateTabContents();
+ TabContents* contents3 = CreateTabContents();
+
+ InsertTabContentses(&tabstrip, contents1, contents2, contents3);
+ EXPECT_EQ(0, tabstrip.selected_index());
+
+ tabstrip.ExecuteContextMenuCommand(2, TabStripModel::CommandCloseTab);
+ EXPECT_EQ(3, tabstrip.count());
+
+ tabstrip.ExecuteContextMenuCommand(0, TabStripModel::CommandCloseTabsToRight);
+ EXPECT_EQ(1, tabstrip.count());
+ EXPECT_EQ(opener_contents, tabstrip.GetSelectedTabContents());
+
+ TabContents* dummy_contents = CreateTabContents();
+ tabstrip.AppendTabContents(dummy_contents, false);
+
+ contents1 = CreateTabContents();
+ contents2 = CreateTabContents();
+ contents3 = CreateTabContents();
+ InsertTabContentses(&tabstrip, contents1, contents2, contents3);
+ EXPECT_EQ(5, tabstrip.count());
+
+ tabstrip.ExecuteContextMenuCommand(0, TabStripModel::CommandCloseTabsOpenedBy);
+ EXPECT_EQ(2, tabstrip.count());
+ EXPECT_EQ(dummy_contents, tabstrip.GetTabContentsAt(1));
+
+ contents1 = CreateTabContents();
+ contents2 = CreateTabContents();
+ contents3 = CreateTabContents();
+ InsertTabContentses(&tabstrip, contents1, contents2, contents3);
+ EXPECT_EQ(5, tabstrip.count());
+
+ int dummy_index = tabstrip.count() - 1;
+ tabstrip.SelectTabContentsAt(dummy_index, true);
+ EXPECT_EQ(dummy_contents, tabstrip.GetSelectedTabContents());
+
+ tabstrip.ExecuteContextMenuCommand(dummy_index,
+ TabStripModel::CommandCloseOtherTabs);
+ EXPECT_EQ(1, tabstrip.count());
+ EXPECT_EQ(dummy_contents, tabstrip.GetSelectedTabContents());
+
+ tabstrip.CloseAllTabs();
+ EXPECT_TRUE(tabstrip.empty());
+}
+
+// Tests whether or not TabContentses are inserted in the correct position
+// using this "smart" function with a simulated middle click action on a series
+// of links on the home page.
+TEST_F(TabStripModelTest, AddTabContents_MiddleClickLinksAndClose) {
+ TabStripModel tabstrip(NULL, profile_);
+ EXPECT_TRUE(tabstrip.empty());
+
+ // Open the Home Page
+ TabContents* homepage_contents = CreateTabContents();
+ tabstrip.AddTabContents(
+ homepage_contents, -1, PageTransition::AUTO_BOOKMARK, true);
+
+ // Open some other tab, by user typing.
+ TabContents* typed_page_contents = CreateTabContents();
+ tabstrip.AddTabContents(
+ typed_page_contents, -1, PageTransition::TYPED, true);
+
+ EXPECT_EQ(2, tabstrip.count());
+
+ // Re-select the home page.
+ tabstrip.SelectTabContentsAt(0, true);
+
+ // Open a bunch of tabs by simulating middle clicking on links on the home
+ // page.
+ TabContents* middle_click_contents1 = CreateTabContents();
+ tabstrip.AddTabContents(
+ middle_click_contents1, -1, PageTransition::LINK, false);
+ TabContents* middle_click_contents2 = CreateTabContents();
+ tabstrip.AddTabContents(
+ middle_click_contents2, -1, PageTransition::LINK, false);
+ TabContents* middle_click_contents3 = CreateTabContents();
+ tabstrip.AddTabContents(
+ middle_click_contents3, -1, PageTransition::LINK, false);
+
+ EXPECT_EQ(5, tabstrip.count());
+
+ EXPECT_EQ(homepage_contents, tabstrip.GetTabContentsAt(0));
+ EXPECT_EQ(middle_click_contents1, tabstrip.GetTabContentsAt(1));
+ EXPECT_EQ(middle_click_contents2, tabstrip.GetTabContentsAt(2));
+ EXPECT_EQ(middle_click_contents3, tabstrip.GetTabContentsAt(3));
+ EXPECT_EQ(typed_page_contents, tabstrip.GetTabContentsAt(4));
+
+ // Now simulate seleting a tab in the middle of the group of tabs opened from
+ // the home page and start closing them. Each TabContents in the group should
+ // be closed, right to left. This test is constructed to start at the middle
+ // TabContents in the group to make sure the cursor wraps around to the first
+ // TabContents in the group before closing the opener or any other
+ // TabContents.
+ tabstrip.SelectTabContentsAt(2, true);
+ tabstrip.CloseSelectedTab();
+ EXPECT_EQ(middle_click_contents3, tabstrip.GetSelectedTabContents());
+ tabstrip.CloseSelectedTab();
+ EXPECT_EQ(middle_click_contents1, tabstrip.GetSelectedTabContents());
+ tabstrip.CloseSelectedTab();
+ EXPECT_EQ(homepage_contents, tabstrip.GetSelectedTabContents());
+ tabstrip.CloseSelectedTab();
+ EXPECT_EQ(typed_page_contents, tabstrip.GetSelectedTabContents());
+
+ EXPECT_EQ(1, tabstrip.count());
+
+ tabstrip.CloseAllTabs();
+ EXPECT_TRUE(tabstrip.empty());
+}
+
+// Tests whether or not a TabContents created by a left click on a link that
+// opens a new tab is inserted correctly adjacent to the tab that spawned it.
+TEST_F(TabStripModelTest, AddTabContents_LeftClickPopup) {
+ TabStripModel tabstrip(NULL, profile_);
+ EXPECT_TRUE(tabstrip.empty());
+
+ // Open the Home Page
+ TabContents* homepage_contents = CreateTabContents();
+ tabstrip.AddTabContents(
+ homepage_contents, -1, PageTransition::AUTO_BOOKMARK, true);
+
+ // Open some other tab, by user typing.
+ TabContents* typed_page_contents = CreateTabContents();
+ tabstrip.AddTabContents(
+ typed_page_contents, -1, PageTransition::TYPED, true);
+
+ EXPECT_EQ(2, tabstrip.count());
+
+ // Re-select the home page.
+ tabstrip.SelectTabContentsAt(0, true);
+
+ // Open a tab by simulating a left click on a link that opens in a new tab.
+ TabContents* left_click_contents = CreateTabContents();
+ tabstrip.AddTabContents(left_click_contents, -1, PageTransition::LINK, true);
+
+ // Verify the state meets our expectations.
+ EXPECT_EQ(3, tabstrip.count());
+ EXPECT_EQ(homepage_contents, tabstrip.GetTabContentsAt(0));
+ EXPECT_EQ(left_click_contents, tabstrip.GetTabContentsAt(1));
+ EXPECT_EQ(typed_page_contents, tabstrip.GetTabContentsAt(2));
+
+ // The newly created tab should be selected.
+ EXPECT_EQ(left_click_contents, tabstrip.GetSelectedTabContents());
+
+ // After closing the selected tab, the selection should move to the left, to
+ // the opener.
+ tabstrip.CloseSelectedTab();
+ EXPECT_EQ(homepage_contents, tabstrip.GetSelectedTabContents());
+
+ EXPECT_EQ(2, tabstrip.count());
+
+ tabstrip.CloseAllTabs();
+ EXPECT_TRUE(tabstrip.empty());
+}
+
+// Tests whether or not new tabs that should split context (typed pages,
+// generated urls, also blank tabs) open at the end of the tabstrip instead of
+// in the middle.
+TEST_F(TabStripModelTest, AddTabContents_CreateNewBlankTab) {
+ TabStripModel tabstrip(NULL, profile_);
+ EXPECT_TRUE(tabstrip.empty());
+
+ // Open the Home Page
+ TabContents* homepage_contents = CreateTabContents();
+ tabstrip.AddTabContents(
+ homepage_contents, -1, PageTransition::AUTO_BOOKMARK, true);
+
+ // Open some other tab, by user typing.
+ TabContents* typed_page_contents = CreateTabContents();
+ tabstrip.AddTabContents(
+ typed_page_contents, -1, PageTransition::TYPED, true);
+
+ EXPECT_EQ(2, tabstrip.count());
+
+ // Re-select the home page.
+ tabstrip.SelectTabContentsAt(0, true);
+
+ // Open a new blank tab in the foreground.
+ TabContents* new_blank_contents = CreateTabContents();
+ tabstrip.AddTabContents(new_blank_contents, -1, PageTransition::TYPED, true);
+
+ // Verify the state of the tabstrip.
+ EXPECT_EQ(3, tabstrip.count());
+ EXPECT_EQ(homepage_contents, tabstrip.GetTabContentsAt(0));
+ EXPECT_EQ(typed_page_contents, tabstrip.GetTabContentsAt(1));
+ EXPECT_EQ(new_blank_contents, tabstrip.GetTabContentsAt(2));
+
+ // Now open a couple more blank tabs in the background.
+ TabContents* background_blank_contents1 = CreateTabContents();
+ tabstrip.AddTabContents(
+ background_blank_contents1, -1, PageTransition::TYPED, false);
+ TabContents* background_blank_contents2 = CreateTabContents();
+ tabstrip.AddTabContents(
+ background_blank_contents2, -1, PageTransition::GENERATED, false);
+ EXPECT_EQ(5, tabstrip.count());
+ EXPECT_EQ(homepage_contents, tabstrip.GetTabContentsAt(0));
+ EXPECT_EQ(typed_page_contents, tabstrip.GetTabContentsAt(1));
+ EXPECT_EQ(new_blank_contents, tabstrip.GetTabContentsAt(2));
+ EXPECT_EQ(background_blank_contents1, tabstrip.GetTabContentsAt(3));
+ EXPECT_EQ(background_blank_contents2, tabstrip.GetTabContentsAt(4));
+
+ tabstrip.CloseAllTabs();
+ EXPECT_TRUE(tabstrip.empty());
+}
+
+// Tests whether opener state is correctly forgotten when the user switches
+// context.
+TEST_F(TabStripModelTest, AddTabContents_ForgetOpeners) {
+ TabStripModel tabstrip(NULL, profile_);
+ EXPECT_TRUE(tabstrip.empty());
+
+ // Open the Home Page
+ TabContents* homepage_contents = CreateTabContents();
+ tabstrip.AddTabContents(
+ homepage_contents, -1, PageTransition::AUTO_BOOKMARK, true);
+
+ // Open some other tab, by user typing.
+ TabContents* typed_page_contents = CreateTabContents();
+ tabstrip.AddTabContents(
+ typed_page_contents, -1, PageTransition::TYPED, true);
+
+ EXPECT_EQ(2, tabstrip.count());
+
+ // Re-select the home page.
+ tabstrip.SelectTabContentsAt(0, true);
+
+ // Open a bunch of tabs by simulating middle clicking on links on the home
+ // page.
+ TabContents* middle_click_contents1 = CreateTabContents();
+ tabstrip.AddTabContents(
+ middle_click_contents1, -1, PageTransition::LINK, false);
+ TabContents* middle_click_contents2 = CreateTabContents();
+ tabstrip.AddTabContents(
+ middle_click_contents2, -1, PageTransition::LINK, false);
+ TabContents* middle_click_contents3 = CreateTabContents();
+ tabstrip.AddTabContents(
+ middle_click_contents3, -1, PageTransition::LINK, false);
+
+ // Break out of the context by selecting a tab in a different context.
+ EXPECT_EQ(typed_page_contents, tabstrip.GetTabContentsAt(4));
+ tabstrip.SelectLastTab();
+ EXPECT_EQ(typed_page_contents, tabstrip.GetSelectedTabContents());
+
+ // Step back into the context by selecting a tab inside it.
+ tabstrip.SelectTabContentsAt(2, true);
+ EXPECT_EQ(middle_click_contents2, tabstrip.GetSelectedTabContents());
+
+ // Now test that closing tabs selects to the right until there are no more,
+ // then to the left, as if there were no context (context has been
+ // successfully forgotten).
+ tabstrip.CloseSelectedTab();
+ EXPECT_EQ(middle_click_contents3, tabstrip.GetSelectedTabContents());
+ tabstrip.CloseSelectedTab();
+ EXPECT_EQ(typed_page_contents, tabstrip.GetSelectedTabContents());
+ tabstrip.CloseSelectedTab();
+ EXPECT_EQ(middle_click_contents1, tabstrip.GetSelectedTabContents());
+ tabstrip.CloseSelectedTab();
+ EXPECT_EQ(homepage_contents, tabstrip.GetSelectedTabContents());
+
+ EXPECT_EQ(1, tabstrip.count());
+
+ tabstrip.CloseAllTabs();
+ EXPECT_TRUE(tabstrip.empty());
+}
+
+class TabStripDummyDelegate : public TabStripModelDelegate {
+ public:
+ explicit TabStripDummyDelegate(TabContents* dummy)
+ : dummy_contents_(dummy) {}
+ virtual ~TabStripDummyDelegate() {}
+
+ // Overridden from TabStripModelDelegate:
+ virtual void CreateNewStripWithContents(TabContents* contents,
+ const gfx::Point& creation_point) {}
+ virtual int GetDragActions() const { return 0; }
+ virtual TabContents* CreateTabContentsForURL(
+ const GURL& url,
+ Profile* profile,
+ PageTransition::Type transition,
+ bool defer_load,
+ SiteInstance* instance) const {
+ if (url == NewTabUIURL())
+ return dummy_contents_;
+ return NULL;
+ }
+ virtual void ShowApplicationMenu(const gfx::Point p) {}
+ virtual bool CanDuplicateContentsAt(int index) { return false; }
+ virtual void DuplicateContentsAt(int index) {}
+ virtual void ValidateLoadingAnimations() {}
+ virtual void CloseFrameAfterDragSession() {}
+
+ private:
+ // A dummy TabContents we give to callers that expect us to actually build a
+ // Destinations tab for them.
+ TabContents* dummy_contents_;
+
+ DISALLOW_EVIL_CONSTRUCTORS(TabStripDummyDelegate);
+};
+
+// Added for http://b/issue?id=958960
+TEST_F(TabStripModelTest, AppendContentsReselectionTest) {
+ TabContents* fake_destinations_tab = CreateTabContents();
+ TabStripDummyDelegate delegate(fake_destinations_tab);
+ TabStripModel tabstrip(&delegate, profile_);
+ EXPECT_TRUE(tabstrip.empty());
+
+ // Open the Home Page
+ TabContents* homepage_contents = CreateTabContents();
+ tabstrip.AddTabContents(
+ homepage_contents, -1, PageTransition::AUTO_BOOKMARK, true);
+
+ // Open some other tab, by user typing.
+ TabContents* typed_page_contents = CreateTabContents();
+ tabstrip.AddTabContents(
+ typed_page_contents, -1, PageTransition::TYPED, false);
+
+ // The selected tab should still be the first.
+ EXPECT_EQ(0, tabstrip.selected_index());
+
+ // Now simulate a link click that opens a new tab (by virtue of target=_blank)
+ // and make sure the right tab gets selected when the new tab is closed.
+ TabContents* target_blank_contents = CreateTabContents();
+ tabstrip.AppendTabContents(target_blank_contents, true);
+ EXPECT_EQ(2, tabstrip.selected_index());
+ tabstrip.CloseTabContentsAt(2);
+ EXPECT_EQ(0, tabstrip.selected_index());
+
+ // Now open a blank tab...
+ tabstrip.AddBlankTab(true);
+ EXPECT_EQ(2, tabstrip.selected_index());
+ tabstrip.CloseTabContentsAt(2);
+ EXPECT_EQ(1, tabstrip.selected_index());
+
+ // clean up after ourselves
+ tabstrip.CloseAllTabs();
+}
+
+// Added for http://b/issue?id=1027661
+TEST_F(TabStripModelTest, ReselectionConsidersChildrenTest) {
+ TabStripDummyDelegate delegate(NULL);
+ TabStripModel strip(&delegate, profile_);
+
+ // Open page A
+ TabContents* page_a_contents = CreateTabContents();
+ strip.AddTabContents(
+ page_a_contents, -1, PageTransition::AUTO_BOOKMARK, true);
+
+ // Simulate middle click to open page A.A and A.B
+ TabContents* page_a_a_contents = CreateTabContents();
+ strip.AddTabContents(page_a_a_contents, -1, PageTransition::LINK, false);
+ TabContents* page_a_b_contents = CreateTabContents();
+ strip.AddTabContents(page_a_b_contents, -1, PageTransition::LINK, false);
+
+ // Select page A.A
+ strip.SelectTabContentsAt(1, true);
+ EXPECT_EQ(page_a_a_contents, strip.GetSelectedTabContents());
+
+ // Simulate a middle click to open page A.A.A
+ TabContents* page_a_a_a_contents = CreateTabContents();
+ strip.AddTabContents(page_a_a_a_contents, -1, PageTransition::LINK, false);
+
+ EXPECT_EQ(page_a_a_a_contents, strip.GetTabContentsAt(2));
+
+ // Close page A.A
+ strip.CloseTabContentsAt(strip.selected_index());
+
+ // Page A.A.A should be selected, NOT A.B
+ EXPECT_EQ(page_a_a_a_contents, strip.GetSelectedTabContents());
+
+ // Close page A.A.A
+ strip.CloseTabContentsAt(strip.selected_index());
+
+ // Page A.B should be selected
+ EXPECT_EQ(page_a_b_contents, strip.GetSelectedTabContents());
+
+ // Close page A.B
+ strip.CloseTabContentsAt(strip.selected_index());
+
+ // Page A should be selected
+ EXPECT_EQ(page_a_contents, strip.GetSelectedTabContents());
+
+ // Clean up.
+ strip.CloseAllTabs();
+}