diff options
| author | initial.commit <initial.commit@0039d316-1c4b-4281-b951-d872f2087c98> | 2008-07-26 23:55:29 +0000 |
|---|---|---|
| committer | initial.commit <initial.commit@0039d316-1c4b-4281-b951-d872f2087c98> | 2008-07-26 23:55:29 +0000 |
| commit | 09911bf300f1a419907a9412154760efd0b7abc3 (patch) | |
| tree | f131325fb4e2ad12c6d3504ab75b16dd92facfed /chrome/browser/tabs | |
| parent | 586acc5fe142f498261f52c66862fa417c3d52d2 (diff) | |
| download | chromium_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.cc | 765 | ||||
| -rw-r--r-- | chrome/browser/tabs/dragged_tab_controller.h | 302 | ||||
| -rw-r--r-- | chrome/browser/tabs/dragged_tab_view.cc | 267 | ||||
| -rw-r--r-- | chrome/browser/tabs/dragged_tab_view.h | 143 | ||||
| -rw-r--r-- | chrome/browser/tabs/hwnd_photobooth.cc | 184 | ||||
| -rw-r--r-- | chrome/browser/tabs/hwnd_photobooth.h | 86 | ||||
| -rw-r--r-- | chrome/browser/tabs/tab.cc | 184 | ||||
| -rw-r--r-- | chrome/browser/tabs/tab.h | 134 | ||||
| -rw-r--r-- | chrome/browser/tabs/tab_renderer.cc | 691 | ||||
| -rw-r--r-- | chrome/browser/tabs/tab_renderer.h | 192 | ||||
| -rw-r--r-- | chrome/browser/tabs/tab_strip.cc | 1385 | ||||
| -rw-r--r-- | chrome/browser/tabs/tab_strip.h | 372 | ||||
| -rw-r--r-- | chrome/browser/tabs/tab_strip_model.cc | 593 | ||||
| -rw-r--r-- | chrome/browser/tabs/tab_strip_model.h | 536 | ||||
| -rw-r--r-- | chrome/browser/tabs/tab_strip_model_order_controller.cc | 146 | ||||
| -rw-r--r-- | chrome/browser/tabs/tab_strip_model_order_controller.h | 75 | ||||
| -rw-r--r-- | chrome/browser/tabs/tab_strip_model_unittest.cc | 1130 |
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(); +} |
