diff options
author | beng@google.com <beng@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2008-09-12 22:44:06 +0000 |
---|---|---|
committer | beng@google.com <beng@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2008-09-12 22:44:06 +0000 |
commit | 3146282c2250c51569d971234fbc9756b4d7a2d4 (patch) | |
tree | 0f9f418993163cd7a67955d10e89928b28787978 /chrome/browser/views/tabs | |
parent | 5db2a6ed0d09ef82cf84ba2677ecfcdff6192c79 (diff) | |
download | chromium_src-3146282c2250c51569d971234fbc9756b4d7a2d4.zip chromium_src-3146282c2250c51569d971234fbc9756b4d7a2d4.tar.gz chromium_src-3146282c2250c51569d971234fbc9756b4d7a2d4.tar.bz2 |
Move View components of the Browser's tab strip into the browser_views project, and into the views/ subdirectory on disk.
B=2198
Review URL: http://codereview.chromium.org/3020
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@2140 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser/views/tabs')
-rw-r--r-- | chrome/browser/views/tabs/dragged_tab_controller.cc | 798 | ||||
-rw-r--r-- | chrome/browser/views/tabs/dragged_tab_controller.h | 286 | ||||
-rw-r--r-- | chrome/browser/views/tabs/dragged_tab_view.cc | 244 | ||||
-rw-r--r-- | chrome/browser/views/tabs/dragged_tab_view.h | 119 | ||||
-rw-r--r-- | chrome/browser/views/tabs/hwnd_photobooth.cc | 159 | ||||
-rw-r--r-- | chrome/browser/views/tabs/hwnd_photobooth.h | 62 | ||||
-rw-r--r-- | chrome/browser/views/tabs/tab.cc | 241 | ||||
-rw-r--r-- | chrome/browser/views/tabs/tab.h | 125 | ||||
-rw-r--r-- | chrome/browser/views/tabs/tab_dragging_test.cc | 507 | ||||
-rw-r--r-- | chrome/browser/views/tabs/tab_renderer.cc | 691 | ||||
-rw-r--r-- | chrome/browser/views/tabs/tab_renderer.h | 174 | ||||
-rw-r--r-- | chrome/browser/views/tabs/tab_strip.cc | 1524 | ||||
-rw-r--r-- | chrome/browser/views/tabs/tab_strip.h | 373 |
13 files changed, 5303 insertions, 0 deletions
diff --git a/chrome/browser/views/tabs/dragged_tab_controller.cc b/chrome/browser/views/tabs/dragged_tab_controller.cc new file mode 100644 index 0000000..81b5386 --- /dev/null +++ b/chrome/browser/views/tabs/dragged_tab_controller.cc @@ -0,0 +1,798 @@ +// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <math.h> + +#include "chrome/browser/views/tabs/dragged_tab_controller.h" + +#include "chrome/browser/browser_window.h" +#include "chrome/browser/frame_util.h" +#include "chrome/browser/tab_contents.h" +#include "chrome/browser/views/tabs/dragged_tab_view.h" +#include "chrome/browser/views/tabs/hwnd_photobooth.h" +#include "chrome/browser/views/tabs/tab.h" +#include "chrome/browser/views/tabs/tab_strip.h" +#include "chrome/browser/web_contents.h" +#include "chrome/views/event.h" +#include "chrome/views/root_view.h" +#include "skia/include/SkBitmap.h" + +static const int kHorizontalMoveThreshold = 16; // pixels + +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_(source_tabstrip), + old_focused_view_(NULL), + in_destructor_(false), + last_move_screen_x_(0) { + ChangeDraggedContents( + source_tabstrip_->model()->GetTabContentsAt(source_model_index_)); + // Listen for Esc key presses. + MessageLoopForUI::current()->AddObserver(this); +} + +DraggedTabController::~DraggedTabController() { + in_destructor_ = true; + CleanUpSourceTab(); + MessageLoopForUI::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 { + if (attached_tabstrip_ == source_tabstrip_) + return contents == dragged_contents_ ? source_tab_ : NULL; + return 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, + const std::string& override_encoding) { + if (original_delegate_) { + if (disposition == CURRENT_TAB) + disposition = NEW_WINDOW; + + original_delegate_->OpenURLFromTab(source, url, disposition, transition, + override_encoding); + } +} + +/////////////////////////////////////////////////////////////////////////////// +// 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_) { + // Determine the horizontal move threshold. This is dependent on the width + // of tabs. The smaller the tabs compared to the standard size, the smaller + // the threshold. + double unselected, selected; + attached_tabstrip_->GetCurrentTabWidths(&unselected, &selected); + double ratio = unselected / Tab::GetStandardSize().width(); + int threshold = static_cast<int>(ratio * kHorizontalMoveThreshold); + + // Update the model, moving the TabContents from one index to another. Do + // this only if we have moved a minimum distance since the last reorder (to + // prevent jitter). + if (abs(screen_point.x() - last_move_screen_x_) > threshold) { + 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); + if (from_index != to_index) { + last_move_screen_x_ = screen_point.x(); + attached_model->MoveTabContentsAt(from_index, to_index); + } + } + } + // Move the View. There are no changes to the model if we're detached. + 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; + + BrowserWindow* other_frame = FrameUtil::GetBrowserWindowForHWND(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(); + attached_tabstrip_->GenerateIdealBounds(); + + // 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(); + + // We need to ask the TabStrip we're attached to to ensure that the ideal + // bounds for all its tabs are correctly generated, because the calculation + // in GetInsertionIndexForDraggedBounds needs them to be to figure out the + // appropriate insertion index. + attached_tabstrip_->GenerateIdealBounds(); + + // Inserting counts as a move. We don't want the tabs to jitter when the + // user moves the tab immediately after attaching it. + last_move_screen_x_ = screen_point.x(); + + // Figure out where to insert the tab based on the bounds of the dragged + // representation and the ideal bounds of the other Tabs already in the + // strip. ("ideal bounds" are stable even if the Tabs' actual bounds are + // changing due to animation). + gfx::Rect bounds = GetDraggedViewTabStripBounds(screen_point); + int index = GetInsertionIndexForDraggedBounds(bounds); + index = std::max(std::min(index, attached_tabstrip_->model()->count()), 0); + 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() { + // We save this here because code below will modify |attached_tabstrip_|. + bool restore_frame = attached_tabstrip_ != source_tabstrip_; + 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); + // TODO(beng): (Cleanup) seems like we should use Attach() for this + // somehow. + attached_tabstrip_ = source_tabstrip_; + 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 { + // TODO(beng): (Cleanup) seems like we should use Attach() for this + // somehow. + attached_tabstrip_ = source_tabstrip_; + // 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 (restore_frame) { + 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. + 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::CleanUpSourceTab() { + // If we were attached to the source TabStrip, source Tab will be in use + // as the Tab. If we were detached or attached to another TabStrip, we can + // safely remove this item and delete it now. + if (attached_tabstrip_ != source_tabstrip_) { + source_tabstrip_->DestroyDraggedSourceTab(source_tab_); + source_tab_ = NULL; + } +} + +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/views/tabs/dragged_tab_controller.h b/chrome/browser/views/tabs/dragged_tab_controller.h new file mode 100644 index 0000000..6c6aed6 --- /dev/null +++ b/chrome/browser/views/tabs/dragged_tab_controller.h @@ -0,0 +1,286 @@ +// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_VIEWS_TABS_DRAGGED_TAB_CONTROLLER_H_ +#define CHROME_BROWSER_VIEWS_TABS_DRAGGED_TAB_CONTROLLER_H_ + +#include "base/gfx/rect.h" +#include "base/message_loop.h" +#include "chrome/browser/tab_contents_delegate.h" +#include "chrome/browser/views/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 MessageLoopForUI::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, + const std::string& override_encoding); + 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(); + + // Cleans up a source tab that is no longer used. + void CleanUpSourceTab(); + + // 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_; + + // The horizontal position of the mouse cursor in screen coordinates at the + // time of the last re-order event. + int last_move_screen_x_; + + DISALLOW_COPY_AND_ASSIGN(DraggedTabController); +}; + +#endif // CHROME_BROWSER_VIEWS_TABS_DRAGGED_TAB_CONTROLLER_H_ + diff --git a/chrome/browser/views/tabs/dragged_tab_view.cc b/chrome/browser/views/tabs/dragged_tab_view.cc new file mode 100644 index 0000000..6e3fead --- /dev/null +++ b/chrome/browser/views/tabs/dragged_tab_view.cc @@ -0,0 +1,244 @@ +// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/views/tabs/dragged_tab_view.h" + +#include "chrome/common/gfx/chrome_canvas.h" +#include "chrome/browser/tab_contents.h" +#include "chrome/browser/tabs/tab_strip_model.h" +#include "chrome/browser/views/tabs/hwnd_photobooth.h" +#include "chrome/browser/views/tabs/tab_renderer.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), false); + container_->SetContentsView(this); +} + +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/views/tabs/dragged_tab_view.h b/chrome/browser/views/tabs/dragged_tab_view.h new file mode 100644 index 0000000..e59582c --- /dev/null +++ b/chrome/browser/views/tabs/dragged_tab_view.h @@ -0,0 +1,119 @@ +// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_VIEWS_TABS_DRAGGED_TAB_VIEW_H_ +#define CHROME_BROWSER_VIEWS_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_VIEWS_TABS_DRAGGED_TAB_VIEW_H_ + diff --git a/chrome/browser/views/tabs/hwnd_photobooth.cc b/chrome/browser/views/tabs/hwnd_photobooth.cc new file mode 100644 index 0000000..a230c6b --- /dev/null +++ b/chrome/browser/views/tabs/hwnd_photobooth.cc @@ -0,0 +1,159 @@ +// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "base/gfx/point.h" +#include "chrome/browser/tab_contents.h" +#include "chrome/browser/views/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, 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); +} diff --git a/chrome/browser/views/tabs/hwnd_photobooth.h b/chrome/browser/views/tabs/hwnd_photobooth.h new file mode 100644 index 0000000..4de1d14 --- /dev/null +++ b/chrome/browser/views/tabs/hwnd_photobooth.h @@ -0,0 +1,62 @@ +// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_VIEWS_TABS_HWND_PHOTOBOOTH_H__ +#define CHROME_BROWSER_VIEWS_TABS_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_VIEWS_TABS_HWND_PHOTOBOOTH_H__ + diff --git a/chrome/browser/views/tabs/tab.cc b/chrome/browser/views/tabs/tab.cc new file mode 100644 index 0000000..bf5dc89 --- /dev/null +++ b/chrome/browser/views/tabs/tab.cc @@ -0,0 +1,241 @@ +// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/views/tabs/tab.h" + +#include "base/gfx/size.h" +#include "chrome/views/view_container.h" +#include "chrome/common/gfx/chrome_canvas.h" +#include "chrome/common/gfx/path.h" +#include "chrome/common/resource_bundle.h" +#include "chrome/views/chrome_menu.h" +#include "chrome/views/tooltip_manager.h" +#include "generated_resources.h" + +const std::string Tab::kTabClassName = "browser/tabs/Tab"; + +static const SkScalar kTabCapWidth = 15; +static const SkScalar kTabTopCurveWidth = 4; +static const SkScalar kTabBottomCurveWidth = 3; + +class TabContextMenuController : public ChromeViews::MenuDelegate { + public: + explicit TabContextMenuController(Tab* tab) + : tab_(tab), + last_command_(TabStripModel::CommandFirst) { + menu_.reset(new ChromeViews::MenuItemView(this)); + menu_->AppendMenuItemWithLabel(TabStripModel::CommandNewTab, + l10n_util::GetString(IDS_TAB_CXMENU_NEWTAB)); + menu_->AppendSeparator(); + menu_->AppendMenuItemWithLabel(TabStripModel::CommandReload, + l10n_util::GetString(IDS_TAB_CXMENU_RELOAD)); + menu_->AppendMenuItemWithLabel( + TabStripModel::CommandDuplicate, + l10n_util::GetString(IDS_TAB_CXMENU_DUPLICATE)); + menu_->AppendSeparator(); + menu_->AppendMenuItemWithLabel( + TabStripModel::CommandCloseTab, + l10n_util::GetString(IDS_TAB_CXMENU_CLOSETAB)); + menu_->AppendMenuItemWithLabel( + TabStripModel::CommandCloseOtherTabs, + l10n_util::GetString(IDS_TAB_CXMENU_CLOSEOTHERTABS)); + menu_->AppendMenuItemWithLabel( + TabStripModel::CommandCloseTabsToRight, + l10n_util::GetString(IDS_TAB_CXMENU_CLOSETABSTORIGHT)); + menu_->AppendMenuItemWithLabel( + TabStripModel::CommandCloseTabsOpenedBy, + l10n_util::GetString(IDS_TAB_CXMENU_CLOSETABSOPENEDBY)); + } + virtual ~TabContextMenuController() { + tab_->delegate()->StopAllHighlighting(); + } + + void RunMenuAt(int x, int y) { + menu_->RunMenuAt(tab_->GetViewContainer()->GetHWND(), + gfx::Rect(x, y, 0, 0), ChromeViews::MenuItemView::TOPLEFT, + false); + } + + private: + // ChromeViews::MenuDelegate implementation: + virtual bool IsCommandEnabled(int id) const { + // The MenuItemView used to contain the contents of the Context Menu itself + // has a command id of 0, and it will check to see if it's enabled for + // some reason during its construction. The TabStripModel can't handle + // command indices it doesn't know about, so we need to filter this out + // here. + if (id == 0) + return false; + return tab_->delegate()->IsCommandEnabledForTab( + static_cast<TabStripModel::ContextMenuCommand>(id), + tab_); + } + + virtual void ExecuteCommand(int id) { + tab_->delegate()->ExecuteCommandForTab( + static_cast<TabStripModel::ContextMenuCommand>(id), + tab_); + } + + virtual void SelectionChanged(ChromeViews::MenuItemView* menu) { + TabStripModel::ContextMenuCommand command = + static_cast<TabStripModel::ContextMenuCommand>(menu->GetCommand()); + tab_->delegate()->StopHighlightTabsForCommand(last_command_, tab_); + last_command_ = command; + tab_->delegate()->StartHighlightTabsForCommand(command, tab_); + } + + private: + // The context menu. + scoped_ptr<ChromeViews::MenuItemView> menu_; + + // The Tab the context menu was brought up for. + Tab* tab_; + + // The last command that was selected, so that we can start/stop highlighting + // appropriately as the user moves through the menu. + TabStripModel::ContextMenuCommand last_command_; + + DISALLOW_EVIL_CONSTRUCTORS(TabContextMenuController); +}; + +/////////////////////////////////////////////////////////////////////////////// +// 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); + SetContextMenuController(this); +} + +Tab::~Tab() { +} + +/////////////////////////////////////////////////////////////////////////////// +// Tab, TabRenderer overrides: + +bool Tab::IsSelected() const { + return delegate_->IsTabSelected(this); +} + +/////////////////////////////////////////////////////////////////////////////// +// Tab, ChromeViews::View overrides: + +bool Tab::HitTest(const CPoint &l) const { + gfx::Path path; + MakePathForTab(&path); + ScopedHRGN rgn(path.CreateHRGN()); + return !!PtInRegion(rgn, l.x, l.y); +} + +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.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::ContextMenuController implementation: + +void Tab::ShowContextMenu(ChromeViews::View* source, int x, int y, + bool is_mouse_gesture) { + TabContextMenuController controller(this); + controller.RunMenuAt(x, y); +} + + +/////////////////////////////////////////////////////////////////////////////// +// ChromeViews::BaseButton::ButtonListener implementation: + +void Tab::ButtonPressed(ChromeViews::BaseButton* sender) { + if (sender == close_button()) + delegate_->CloseTab(this); +} + +/////////////////////////////////////////////////////////////////////////////// +// Tab, private: + +void Tab::MakePathForTab(gfx::Path* path) const { + DCHECK(path); + + SkScalar h = SkIntToScalar(GetHeight()); + SkScalar w = SkIntToScalar(GetWidth()); + + path->moveTo(0, h); + + // Left end cap. + path->lineTo(kTabBottomCurveWidth, h - kTabBottomCurveWidth); + path->lineTo(kTabCapWidth - kTabTopCurveWidth, kTabTopCurveWidth); + path->lineTo(kTabCapWidth, 0); + + // Connect to the right cap. + path->lineTo(w - kTabCapWidth, 0); + + // Right end cap. + path->lineTo(w - kTabCapWidth - kTabTopCurveWidth, kTabTopCurveWidth); + path->lineTo(w - kTabBottomCurveWidth, h - kTabBottomCurveWidth); + path->lineTo(w, h); + + // Close out the path. + path->lineTo(0, h); + path->close(); +} diff --git a/chrome/browser/views/tabs/tab.h b/chrome/browser/views/tabs/tab.h new file mode 100644 index 0000000..753e1a9 --- /dev/null +++ b/chrome/browser/views/tabs/tab.h @@ -0,0 +1,125 @@ +// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_VIEWS_TABS_TAB_H_ +#define CHROME_BROWSER_VIEWS_TABS_TAB_H_ + +#include "chrome/browser/tabs/tab_strip_model.h" +#include "chrome/browser/views/tabs/tab_renderer.h" +#include "chrome/views/base_button.h" + +namespace gfx { +class Path; +class Point; +} +class TabContents; +class Profile; + +/////////////////////////////////////////////////////////////////////////////// +// +// Tab +// +// A subclass of TabRenderer that represents an individual Tab in a TabStrip. +// +/////////////////////////////////////////////////////////////////////////////// +class Tab : public TabRenderer, + public ChromeViews::ContextMenuController, + 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; + + // Starts/Stops highlighting the tabs that will be affected by the + // specified command for the specified Tab. + virtual void StartHighlightTabsForCommand( + TabStripModel::ContextMenuCommand command_id, Tab* tab) = 0; + virtual void StopHighlightTabsForCommand( + TabStripModel::ContextMenuCommand command_id, Tab* tab) = 0; + virtual void StopAllHighlighting() = 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(); + + // Access the delegate. + TabDelegate* delegate() const { return delegate_; } + + // 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; + + // ChromeViews::View overrides: + virtual bool HitTest(const CPoint &l) 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::ContextMenuController overrides: + virtual void ShowContextMenu(ChromeViews::View* source, + int x, + int y, + bool is_mouse_gesture); + + // ChromeViews::BaseButton::ButtonListener overrides: + virtual void ButtonPressed(ChromeViews::BaseButton* sender); + + // Creates a path that contains the clickable region of the tab's visual + // representation. Used by GetViewForPoint for hit-testing. + void MakePathForTab(gfx::Path* path) const; + + // 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_COPY_AND_ASSIGN(Tab); +}; + +#endif // CHROME_BROWSER_VIEWS_TABS_TAB_H_ + diff --git a/chrome/browser/views/tabs/tab_dragging_test.cc b/chrome/browser/views/tabs/tab_dragging_test.cc new file mode 100644 index 0000000..631a0f0 --- /dev/null +++ b/chrome/browser/views/tabs/tab_dragging_test.cc @@ -0,0 +1,507 @@ +// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "base/command_line.h" +#include "base/file_util.h" +#include "base/time.h" +#include "chrome/app/chrome_dll_resource.h" +#include "chrome/browser/view_ids.h" +#include "chrome/common/chrome_paths.h" +#include "chrome/common/chrome_switches.h" +#include "chrome/common/pref_names.h" +#include "chrome/test/automation/tab_proxy.h" +#include "chrome/test/automation/browser_proxy.h" +#include "chrome/test/automation/window_proxy.h" +#include "chrome/test/ui/ui_test.h" +#include "chrome/views/event.h" +#include "googleurl/src/gurl.h" +#include "net/base/net_util.h" + + +class TabDraggingTest : public UITest { +protected: + TabDraggingTest() { + show_window_ = true; + } +}; + +// Automated UI test to open three tabs in a new window, and drag Tab_1 into +// the position of Tab_2. +TEST_F(TabDraggingTest, Tab1Tab2) { + scoped_ptr<BrowserProxy> browser(automation()->GetBrowserWindow(0)); + ASSERT_TRUE(browser.get()); + scoped_ptr<WindowProxy> window( + automation()->GetWindowForBrowser(browser.get())); + ASSERT_TRUE(window.get()); + + // Get initial tab count. + int initial_tab_count = 0; + ASSERT_TRUE(browser->GetTabCount(&initial_tab_count)); + ASSERT_TRUE(1 == initial_tab_count); + + // Get Tab_1 which comes with the browser window. + scoped_ptr<TabProxy> tab1(browser->GetTab(0)); + ASSERT_TRUE(tab1.get()); + GURL tab1_url; + ASSERT_TRUE(tab1->GetCurrentURL(&tab1_url)); + + // Add Tab_2. + GURL tab2_url("about:"); + ASSERT_TRUE(browser->AppendTab(tab2_url)); + scoped_ptr<TabProxy> tab2(browser->GetTab(1)); + ASSERT_TRUE(tab2.get()); + + // Add Tab_3. + GURL tab3_url("about:plugins"); + ASSERT_TRUE(browser->AppendTab(tab3_url)); + scoped_ptr<TabProxy> tab3(browser->GetTab(2)); + ASSERT_TRUE(tab3.get()); + + // Make sure 3 tabs are open + int final_tab_count = 0; + ASSERT_TRUE(browser->WaitForTabCountToChange(initial_tab_count, + &final_tab_count, + 10000)); + ASSERT_TRUE(final_tab_count == initial_tab_count + 2); + + // Get bounds for the tabs. + gfx::Rect bounds1; + ASSERT_TRUE(window->GetViewBounds(VIEW_ID_TAB_0, &bounds1, false)); + EXPECT_LT(0, bounds1.x()); + EXPECT_LT(0, bounds1.width()); + EXPECT_LT(0, bounds1.height()); + + gfx::Rect bounds2; + ASSERT_TRUE(window->GetViewBounds(VIEW_ID_TAB_1, &bounds2, false)); + EXPECT_LT(0, bounds2.width()); + EXPECT_LT(0, bounds2.height()); + EXPECT_LT(bounds1.x(), bounds2.x()); + EXPECT_EQ(bounds2.y(), bounds1.y()); + + gfx::Rect bounds3; + ASSERT_TRUE(window->GetViewBounds(VIEW_ID_TAB_2, &bounds3, false)); + EXPECT_LT(0, bounds3.width()); + EXPECT_LT(0, bounds3.height()); + EXPECT_LT(bounds2.x(), bounds3.x()); + EXPECT_EQ(bounds3.y(), bounds2.y()); + + // Get url Bar bounds. + gfx::Rect urlbar_bounds; + ASSERT_TRUE(window->GetViewBounds(VIEW_ID_LOCATION_BAR, &urlbar_bounds, + false)); + EXPECT_LT(0, urlbar_bounds.x()); + EXPECT_LT(0, urlbar_bounds.y()); + EXPECT_LT(0, urlbar_bounds.width()); + EXPECT_LT(0, urlbar_bounds.height()); + + // TEST: Move Tab_1 to the position of Tab_2 + // ____________ ____________ ____________ + // / \ / \ / \ + // | Tab_1 | Tab_2 | Tab_3 | + // ---- ---- ---- ---- ---- ---- ---- ---- ---- + // x---- ----> + // ____________ + // / X \ + // | Tab_1 | + // ---- ---- ---- + + POINT start; + POINT end; + start.x = bounds1.x() + bounds1.width()/2; + start.y = bounds1.y() + bounds1.height()/2; + end.x = start.x + 2*bounds1.width()/3; + end.y = start.y; + ASSERT_TRUE(browser->SimulateDrag(start, end, + ChromeViews::Event::EF_LEFT_BUTTON_DOWN, + false)); + + // Now check for expected results. + tab1.reset(browser->GetTab(0)); + ASSERT_TRUE(tab1.get()); + GURL tab1_new_url; + ASSERT_TRUE(tab1->GetCurrentURL(&tab1_new_url)); + + tab2.reset(browser->GetTab(1)); + ASSERT_TRUE(tab2.get()); + GURL tab2_new_url; + ASSERT_TRUE(tab2->GetCurrentURL(&tab2_new_url)); + + EXPECT_EQ(tab1_url.spec(), tab2_new_url.spec()); + EXPECT_EQ(tab2_url.spec(), tab1_new_url.spec()); +} + +// Drag Tab_1 into the position of Tab_3. +TEST_F(TabDraggingTest, Tab1Tab3) { + scoped_ptr<BrowserProxy> browser(automation()->GetBrowserWindow(0)); + ASSERT_TRUE(browser.get()); + scoped_ptr<WindowProxy> window( + automation()->GetWindowForBrowser(browser.get())); + ASSERT_TRUE(window.get()); + + // Get initial tab count. + int initial_tab_count = 0; + ASSERT_TRUE(browser->GetTabCount(&initial_tab_count)); + ASSERT_TRUE(1 == initial_tab_count); + + // Get Tab_1 which comes with the browser window. + scoped_ptr<TabProxy> tab1(browser->GetTab(0)); + ASSERT_TRUE(tab1.get()); + GURL tab1_url; + ASSERT_TRUE(tab1->GetCurrentURL(&tab1_url)); + + // Add Tab_2. + GURL tab2_url("about:"); + ASSERT_TRUE(browser->AppendTab(tab2_url)); + scoped_ptr<TabProxy> tab2(browser->GetTab(1)); + ASSERT_TRUE(tab2.get()); + + // Add Tab_3. + GURL tab3_url("about:plugins"); + ASSERT_TRUE(browser->AppendTab(tab3_url)); + scoped_ptr<TabProxy> tab3(browser->GetTab(2)); + ASSERT_TRUE(tab3.get()); + + // Make sure 3 tabs are open + int final_tab_count = 0; + ASSERT_TRUE(browser->WaitForTabCountToChange(initial_tab_count, + &final_tab_count, + 10000)); + ASSERT_TRUE(final_tab_count == initial_tab_count + 2); + + // Get bounds for the tabs. + gfx::Rect bounds1; + ASSERT_TRUE(window->GetViewBounds(VIEW_ID_TAB_0, &bounds1, false)); + EXPECT_LT(0, bounds1.x()); + EXPECT_LT(0, bounds1.width()); + EXPECT_LT(0, bounds1.height()); + + gfx::Rect bounds2; + ASSERT_TRUE(window->GetViewBounds(VIEW_ID_TAB_1, &bounds2, false)); + EXPECT_LT(0, bounds2.width()); + EXPECT_LT(0, bounds2.height()); + EXPECT_LT(bounds1.x(), bounds2.x()); + EXPECT_EQ(bounds2.y(), bounds1.y()); + + gfx::Rect bounds3; + ASSERT_TRUE(window->GetViewBounds(VIEW_ID_TAB_2, &bounds3, false)); + EXPECT_LT(0, bounds3.width()); + EXPECT_LT(0, bounds3.height()); + EXPECT_LT(bounds2.x(), bounds3.x()); + EXPECT_EQ(bounds3.y(), bounds2.y()); + + // Get url Bar bounds. + gfx::Rect urlbar_bounds; + ASSERT_TRUE(window->GetViewBounds(VIEW_ID_LOCATION_BAR, &urlbar_bounds, + false)); + EXPECT_LT(0, urlbar_bounds.x()); + EXPECT_LT(0, urlbar_bounds.y()); + EXPECT_LT(0, urlbar_bounds.width()); + EXPECT_LT(0, urlbar_bounds.height()); + + // TEST: Move Tab_1 to the middle position of Tab_3 + // ____________ ____________ ____________ + // / \ / \ / \ + // | Tab_1 | Tab_2 | Tab_3 | + // ---- ---- ---- ---- ---- ---- ---- ---- ---- + // x---- ---- ---- ---- ---- ----> + // ____________ + // / X \ + // | Tab_1 | + // ---- ---- ---- + + POINT start; + POINT end; + start.x = bounds1.x() + bounds1.width()/2; + start.y = bounds1.y() + bounds1.height()/2; + end.x = start.x + bounds1.width()/2 + bounds2.width() + bounds3.width()/2; + end.y = start.y; + ASSERT_TRUE(browser->SimulateDrag(start, end, + ChromeViews::Event::EF_LEFT_BUTTON_DOWN, + false)); + + // Now check for expected results. + tab1.reset(browser->GetTab(0)); + ASSERT_TRUE(tab1.get()); + GURL tab1_new_url; + ASSERT_TRUE(tab1->GetCurrentURL(&tab1_new_url)); + + tab2.reset(browser->GetTab(1)); + ASSERT_TRUE(tab2.get()); + GURL tab2_new_url; + ASSERT_TRUE(tab2->GetCurrentURL(&tab2_new_url)); + + tab3.reset(browser->GetTab(2)); + ASSERT_TRUE(tab3.get()); + GURL tab3_new_url; + ASSERT_TRUE(tab3->GetCurrentURL(&tab3_new_url)); + + EXPECT_EQ(tab1_new_url.spec(), tab2_url.spec()); + EXPECT_EQ(tab2_new_url.spec(), tab3_url.spec()); + EXPECT_EQ(tab3_new_url.spec(), tab1_url.spec()); +} + +// Drag Tab_1 into the position of Tab_3, and press ESCAPE before releasing the +// left mouse button. +TEST_F(TabDraggingTest, Tab1Tab3Escape) { + scoped_ptr<BrowserProxy> browser(automation()->GetBrowserWindow(0)); + ASSERT_TRUE(browser.get()); + scoped_ptr<WindowProxy> window( + automation()->GetWindowForBrowser(browser.get())); + ASSERT_TRUE(window.get()); + + // Get initial tab count. + int initial_tab_count = 0; + ASSERT_TRUE(browser->GetTabCount(&initial_tab_count)); + ASSERT_TRUE(1 == initial_tab_count); + + // Get Tab_1 which comes with the browser window. + scoped_ptr<TabProxy> tab1(browser->GetTab(0)); + ASSERT_TRUE(tab1.get()); + GURL tab1_url; + ASSERT_TRUE(tab1->GetCurrentURL(&tab1_url)); + + // Add Tab_2. + GURL tab2_url("about:"); + ASSERT_TRUE(browser->AppendTab(tab2_url)); + scoped_ptr<TabProxy> tab2(browser->GetTab(1)); + ASSERT_TRUE(tab2.get()); + + // Add Tab_3. + GURL tab3_url("about:plugins"); + ASSERT_TRUE(browser->AppendTab(tab3_url)); + scoped_ptr<TabProxy> tab3(browser->GetTab(2)); + ASSERT_TRUE(tab3.get()); + + // Make sure 3 tabs are open + int final_tab_count = 0; + ASSERT_TRUE(browser->WaitForTabCountToChange(initial_tab_count, + &final_tab_count, + 10000)); + ASSERT_TRUE(final_tab_count == initial_tab_count + 2); + + // Get bounds for the tabs. + gfx::Rect bounds1; + ASSERT_TRUE(window->GetViewBounds(VIEW_ID_TAB_0, &bounds1, false)); + EXPECT_LT(0, bounds1.x()); + EXPECT_LT(0, bounds1.width()); + EXPECT_LT(0, bounds1.height()); + + gfx::Rect bounds2; + ASSERT_TRUE(window->GetViewBounds(VIEW_ID_TAB_1, &bounds2, false)); + EXPECT_LT(0, bounds2.width()); + EXPECT_LT(0, bounds2.height()); + EXPECT_LT(bounds1.x(), bounds2.x()); + EXPECT_EQ(bounds2.y(), bounds1.y()); + + gfx::Rect bounds3; + ASSERT_TRUE(window->GetViewBounds(VIEW_ID_TAB_2, &bounds3, false)); + EXPECT_LT(0, bounds3.width()); + EXPECT_LT(0, bounds3.height()); + EXPECT_LT(bounds2.x(), bounds3.x()); + EXPECT_EQ(bounds3.y(), bounds2.y()); + + // Get url Bar bounds. + gfx::Rect urlbar_bounds; + ASSERT_TRUE(window->GetViewBounds(VIEW_ID_LOCATION_BAR, &urlbar_bounds, + false)); + EXPECT_LT(0, urlbar_bounds.x()); + EXPECT_LT(0, urlbar_bounds.y()); + EXPECT_LT(0, urlbar_bounds.width()); + EXPECT_LT(0, urlbar_bounds.height()); + + // TEST: Move Tab_1 to the middle position of Tab_3 + // ____________ ____________ ____________ + // / \ / \ / \ + // | Tab_1 | Tab_2 | Tab_3 | + // ---- ---- ---- ---- ---- ---- ---- ---- ---- + // x---- ---- ---- ---- ---- ----> + ESCAPE + // ____________ + // / X \ + // | Tab_1 | + // ---- ---- ---- + + POINT start; + POINT end; + start.x = bounds1.x() + bounds1.width()/2; + start.y = bounds1.y() + bounds1.height()/2; + end.x = start.x + bounds1.width()/2 + bounds2.width() + bounds3.width()/2; + end.y = start.y; + + // Simulate drag with 'true' as the last parameter. This will interrupt + // in-flight with Escape. + ASSERT_TRUE(browser->SimulateDrag(start, end, + ChromeViews::Event::EF_LEFT_BUTTON_DOWN, + true)); + + // Now check for expected results. + tab1.reset(browser->GetTab(0)); + ASSERT_TRUE(tab1.get()); + GURL tab1_new_url; + ASSERT_TRUE(tab1->GetCurrentURL(&tab1_new_url)); + + tab2.reset(browser->GetTab(1)); + ASSERT_TRUE(tab2.get()); + GURL tab2_new_url; + ASSERT_TRUE(tab2->GetCurrentURL(&tab2_new_url)); + + tab3.reset(browser->GetTab(2)); + ASSERT_TRUE(tab3.get()); + GURL tab3_new_url; + ASSERT_TRUE(tab3->GetCurrentURL(&tab3_new_url)); + + // The tabs should be in their original positions. + EXPECT_EQ(tab1_new_url.spec(), tab1_url.spec()); + EXPECT_EQ(tab2_new_url.spec(), tab2_url.spec()); + EXPECT_EQ(tab3_new_url.spec(), tab3_url.spec()); +} + +// Drag Tab_2 out of the Tab strip. A new window should open with this tab. +TEST_F(TabDraggingTest, Tab2OutOfTabStrip) { + scoped_ptr<BrowserProxy> browser(automation()->GetBrowserWindow(0)); + ASSERT_TRUE(browser.get()); + scoped_ptr<WindowProxy> window( + automation()->GetWindowForBrowser(browser.get())); + ASSERT_TRUE(window.get()); + + // Get initial tab count. + int initial_tab_count = 0; + ASSERT_TRUE(browser->GetTabCount(&initial_tab_count)); + ASSERT_TRUE(1 == initial_tab_count); + + // Get Tab_1 which comes with the browser window. + scoped_ptr<TabProxy> tab1(browser->GetTab(0)); + ASSERT_TRUE(tab1.get()); + GURL tab1_url; + ASSERT_TRUE(tab1->GetCurrentURL(&tab1_url)); + + // Add Tab_2. + GURL tab2_url("about:version"); + ASSERT_TRUE(browser->AppendTab(tab2_url)); + scoped_ptr<TabProxy> tab2(browser->GetTab(1)); + ASSERT_TRUE(tab2.get()); + + // Add Tab_3. + GURL tab3_url("about:plugins"); + ASSERT_TRUE(browser->AppendTab(tab3_url)); + scoped_ptr<TabProxy> tab3(browser->GetTab(2)); + ASSERT_TRUE(tab3.get()); + + // Make sure 3 tabs are opened. + int final_tab_count = 0; + ASSERT_TRUE(browser->WaitForTabCountToChange(initial_tab_count, + &final_tab_count, + 10000)); + ASSERT_TRUE(final_tab_count == initial_tab_count + 2); + + // Make sure all the tab URL specs are different. + ASSERT_TRUE(tab1_url != tab2_url); + ASSERT_TRUE(tab1_url != tab3_url); + ASSERT_TRUE(tab2_url != tab3_url); + + // Get bounds for the tabs. + gfx::Rect bounds1; + ASSERT_TRUE(window->GetViewBounds(VIEW_ID_TAB_0, &bounds1, false)); + EXPECT_LT(0, bounds1.x()); + EXPECT_LT(0, bounds1.width()); + EXPECT_LT(0, bounds1.height()); + + gfx::Rect bounds2; + ASSERT_TRUE(window->GetViewBounds(VIEW_ID_TAB_1, &bounds2, false)); + EXPECT_LT(0, bounds2.width()); + EXPECT_LT(0, bounds2.height()); + EXPECT_LT(bounds1.x(), bounds2.x()); + EXPECT_EQ(bounds2.y(), bounds1.y()); + + gfx::Rect bounds3; + ASSERT_TRUE(window->GetViewBounds(VIEW_ID_TAB_2, &bounds3, false)); + EXPECT_LT(0, bounds3.width()); + EXPECT_LT(0, bounds3.height()); + EXPECT_LT(bounds2.x(), bounds3.x()); + EXPECT_EQ(bounds3.y(), bounds2.y()); + + // Get url Bar bounds. + gfx::Rect urlbar_bounds; + ASSERT_TRUE(window->GetViewBounds(VIEW_ID_LOCATION_BAR, &urlbar_bounds, + false)); + EXPECT_LT(0, urlbar_bounds.x()); + EXPECT_LT(0, urlbar_bounds.y()); + EXPECT_LT(0, urlbar_bounds.width()); + EXPECT_LT(0, urlbar_bounds.height()); + + // TEST: Move Tab_2 down, out of the tab strip. + // This should result in the following: + // 1- Tab_3 shift left in place of Tab_2 in Window 1 + // 2- Tab_1 to remain in its place + // 3- Tab_2 openes in a new window + // + // ____________ ____________ ____________ + // / \ / \ / \ + // | Tab_1 | Tab_2 | Tab_3 | + // ---- ---- ---- ---- ---- ---- ---- ---- ---- + // x + // | + // | (Drag this below, out of tab strip) + // V + // ____________ + // / X \ + // | Tab_2 | (New Window) + // ---- ---- ---- ---- ---- ---- ---- + + POINT start; + POINT end; + start.x = bounds2.x() + bounds2.width()/2; + start.y = bounds2.y() + bounds2.height()/2; + end.x = start.x; + end.y = start.y + 3*urlbar_bounds.height(); + + // Simulate tab drag. + ASSERT_TRUE(browser->SimulateDrag(start, end, + ChromeViews::Event::EF_LEFT_BUTTON_DOWN, + false)); + + // Now, first make sure that the old window has only two tabs remaining. + int new_tab_count = 0; + ASSERT_TRUE(browser->GetTabCount(&new_tab_count)); + ASSERT_EQ(2, new_tab_count); + + // Get the two tabs - they are called Tab_1 and Tab_2 in the old window. + tab1.reset(browser->GetTab(0)); + ASSERT_TRUE(tab1.get()); + GURL tab1_new_url; + ASSERT_TRUE(tab1->GetCurrentURL(&tab1_new_url)); + + tab2.reset(browser->GetTab(1)); + ASSERT_TRUE(tab2.get()); + GURL tab2_new_url; + ASSERT_TRUE(tab2->GetCurrentURL(&tab2_new_url)); + + // Now check for proper shifting of tabs; i.e., Tab_3 in window 1 should + // shift left to the position of Tab_2; Tab_1 should stay where it was. + EXPECT_EQ(tab1_new_url.spec(), tab1_url.spec()); + EXPECT_EQ(tab2_new_url.spec(), tab3_url.spec()); + + // Now check to make sure a new window has opened. + scoped_ptr<BrowserProxy> browser2(automation()->GetBrowserWindow(1)); + ASSERT_TRUE(browser2.get()); + scoped_ptr<WindowProxy> window2( + automation()->GetWindowForBrowser(browser2.get())); + ASSERT_TRUE(window2.get()); + + // Make sure that the new window has only one tab. + int tab_count_window_2 = 0; + ASSERT_TRUE(browser2->GetTabCount(&tab_count_window_2)); + ASSERT_EQ(1, tab_count_window_2); + + // Get Tab_1_2 which should be Tab_1 in Window 2. + scoped_ptr<TabProxy> tab1_2(browser2->GetTab(0)); + ASSERT_TRUE(tab1_2.get()); + GURL tab1_2_url; + ASSERT_TRUE(tab1_2->GetCurrentURL(&tab1_2_url)); + + // Tab_1_2 of Window 2 should essentially be Tab_2 of Window 1. + EXPECT_EQ(tab1_2_url.spec(), tab2_url.spec()); + EXPECT_NE(tab1_2_url.spec(), tab1_url.spec()); + EXPECT_NE(tab1_2_url.spec(), tab3_url.spec()); +} + diff --git a/chrome/browser/views/tabs/tab_renderer.cc b/chrome/browser/views/tabs/tab_renderer.cc new file mode 100644 index 0000000..d7509ab --- /dev/null +++ b/chrome/browser/views/tabs/tab_renderer.cc @@ -0,0 +1,691 @@ +// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <limits> + +#include "chrome/browser/views/tabs/tab_renderer.h" + +#include "base/gfx/image_operations.h" +#include "chrome/app/theme/theme_resources.h" +#include "chrome/browser/browser.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 long the pulse throb takes. +static const int kPulseDurationMs = 200; + +// 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); + + pulse_animation_.reset(new ThrobAnimation(this)); + pulse_animation_->SetSlideDuration(kPulseDurationMs); +} + +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::StartPulse() { + pulse_animation_->Reset(); + pulse_animation_->StartThrobbing(std::numeric_limits<int>::max()); +} + +void TabRenderer::StopPulse() { + if (pulse_animation_->IsAnimating()) + pulse_animation_->Stop(); +} + +// 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; +} + +//////////////////////////////////////////////////////////////////////////////// +// 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(); + + PaintTabBackground(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 { + Browser::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, AnimationDelegate implementation: + +void TabRenderer::AnimationProgressed(const Animation* animation) { + SchedulePaint(); +} + +void TabRenderer::AnimationCanceled(const Animation* animation) { + AnimationEnded(animation); +} + +void TabRenderer::AnimationEnded(const Animation* animation) { + SchedulePaint(); +} + +//////////////////////////////////////////////////////////////////////////////// +// TabRenderer, private + +void TabRenderer::PaintTabBackground(ChromeCanvas* canvas) { + 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. + Animation* animation = hover_animation_.get(); + if (pulse_animation_->IsAnimating()) + animation = pulse_animation_.get(); + if (animation->GetCurrentValue() > 0) { + PaintHoverTabBackground(canvas, animation->GetCurrentValue() * + (win_util::ShouldUseVistaFrame() ? + kHoverOpacityVista : kHoverOpacity)); + } else { + PaintInactiveTabBackground(canvas); + } + } +} + +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/views/tabs/tab_renderer.h b/chrome/browser/views/tabs/tab_renderer.h new file mode 100644 index 0000000..cb92574 --- /dev/null +++ b/chrome/browser/views/tabs/tab_renderer.h @@ -0,0 +1,174 @@ +// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_VIEWS_TABS_TAB_RENDERER_H__ +#define CHROME_BROWSER_VIEWS_TABS_TAB_RENDERER_H__ + +#include "base/gfx/point.h" +#include "chrome/common/animation.h" +#include "chrome/common/slide_animation.h" +#include "chrome/common/throb_animation.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); + + // Starts/Stops a pulse animation. + void StartPulse(); + void StopPulse(); + + // 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(); + + 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); + + // Overridden from AnimationDelegate: + virtual void AnimationProgressed(const Animation* animation); + virtual void AnimationCanceled(const Animation* animation); + virtual void AnimationEnded(const Animation* animation); + + // 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 PaintTabBackground(ChromeCanvas* canvas); + 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_; + + // Pulse animation. + scoped_ptr<ThrobAnimation> pulse_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_VIEWS_TABS_TAB_RENDERER_H__ + diff --git a/chrome/browser/views/tabs/tab_strip.cc b/chrome/browser/views/tabs/tab_strip.cc new file mode 100644 index 0000000..2b8dc61 --- /dev/null +++ b/chrome/browser/views/tabs/tab_strip.cc @@ -0,0 +1,1524 @@ +// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/views/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/tab_strip_model.h" +#include "chrome/browser/user_metrics.h" +#include "chrome/browser/view_ids.h" +#include "chrome/browser/views/tabs/dragged_tab_controller.h" +#include "chrome/browser/views/tabs/tab.h" +#include "chrome/browser/vista_frame.h" +#include "chrome/browser/web_contents.h" +#include "chrome/common/drag_drop_types.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; + + // Possible types of animation. + enum Type { + INSERT, + REMOVE, + MOVE, + RESIZE + }; + + TabAnimation(TabStrip* tabstrip, Type type) + : tabstrip_(tabstrip), + animation_(this), + start_selected_width_(0), + start_unselected_width_(0), + end_selected_width_(0), + end_unselected_width_(0), + layout_on_completion_(false), + type_(type) { + } + virtual ~TabAnimation() {} + + Type type() const { return type_; } + + 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_); + double standard_tab_width = + static_cast<double>(TabRenderer::GetStandardSize().width()); + if (start_tab_count < end_tab_count && + start_unselected_width_ < standard_tab_width) { + double minimum_tab_width = + static_cast<double>(TabRenderer::GetMinimumSize().width()); + start_unselected_width_ -= minimum_tab_width / start_tab_count; + } + 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_; + + const Type type_; + + 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, INSERT), + 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, REMOVE), + index_(index) { + int tab_count = tabstrip->GetTabCount(); + GenerateStartAndEndWidths(tab_count, tab_count - 1); + } + + // Returns the index of the tab being removed. + int index() const { return index_; } + + 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, MOVE), + 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: + explicit ResizeLayoutAnimation(TabStrip* tabstrip) + : TabAnimation(tabstrip, RESIZE) { + 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() { + // TODO(beng): (1031854) Restore this line once XPFrame/VistaFrame are dead. + //model_->RemoveObserver(this); + + // TODO(beng): remove this if it doesn't work to fix the TabSelectedAt bug. + drag_controller_.reset(NULL); + + // 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::PointIsWithinWindowCaption(const CPoint& point) { + ChromeViews::View* v = GetViewForPoint(point); + + // If there is no control at this location, claim the hit was in the title + // bar to get a move action. + if (v == this) + return true; + + // If the point is within the bounds of a Tab, the point can be considered + // part of the caption if there are no available drag operations for the Tab. + if (v->GetClassName() == Tab::kTabClassName && !HasAvailableDragActions()) + return true; + + // All other regions, including the new Tab button, should be considered part + // of the containing Window's client area so that regular events can be + // processed for them. + return false; +} + +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); +} + +void TabStrip::DestroyDraggedSourceTab(Tab* tab) { + // We could be running an animation that references this Tab. + if (active_animation_.get()) + active_animation_->Stop(); + // Make sure we leave the tab_data_ vector in a consistent state, otherwise + // we'll be pointing to tabs that have been deleted and removed from the + // child view list. + std::vector<TabData>::iterator it = tab_data_.begin(); + for (; it != tab_data_.end(); ++it) { + if (it->tab == tab) { + NOTREACHED() << "Leaving in an inconsistent state!"; + tab_data_.erase(it); + break; + } + } + tab->GetParent()->RemoveChildView(tab); + delete tab; + // Force a layout here, because if we've just quickly drag detached a Tab, + // the stopping of the active animation above may have left the TabStrip in a + // bad (visual) state. + Layout(); +} + +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); +} + +ChromeViews::View* TabStrip::GetViewForPoint(const CPoint& point) { + return GetViewForPoint(point, false); +} + +ChromeViews::View* TabStrip::GetViewForPoint(const CPoint& point, + bool can_create_floating) { + // Return any view that isn't a Tab or this TabStrip immediately. We don't + // want to interfere. + ChromeViews::View* v = View::GetViewForPoint(point, can_create_floating); + if (v && v != this && v->GetClassName() != Tab::kTabClassName) + return v; + + // The display order doesn't necessarily match the child list order, so we + // walk the display list hit-testing Tabs. Since the selected tab always + // renders on top of adjacent tabs, it needs to be hit-tested before any + // left-adjacent Tab, so we look ahead for it as we walk. + int tab_count = GetTabCount(); + for (int i = 0; i < tab_count; ++i) { + Tab* next_tab = i < (tab_count - 1) ? GetTabAt(i + 1) : NULL; + if (next_tab && next_tab->IsSelected() && IsPointInTab(next_tab, point)) + return next_tab; + Tab* tab = GetTabAt(i); + if (IsPointInTab(tab, point)) + return tab; + } + + // No need to do any floating view stuff, we don't use them in the TabStrip. + return this; +} + +/////////////////////////////////////////////////////////////////////////////// +// 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(); + + bool contains_tab = false; + 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); + tab->SetVisible(true); + } + + // See if we're already in the list. We don't want to add ourselves twice. + std::vector<TabData>::const_iterator iter = tab_data_.begin(); + for (; iter != tab_data_.end() && !contains_tab; ++iter) { + if (iter->tab == tab) + contains_tab = true; + } + } + + // Otherwise we need to make a new Tab. + if (!tab) + tab = new Tab(this); + + // Only insert if we're not already in the list. + if (!contains_tab) { + 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) { + // Index is in terms of the model. Need to make sure we adjust that index in + // case we have an animation going. + Tab* tab = GetTabAtAdjustForAnimation(index); + tab->UpdateData(contents); + tab->UpdateFromModel(); +} + +void TabStrip::TabValidateAnimations() { + if (model_->TabsAreLoading()) { + if (!loading_animation_timer_.IsRunning()) { + // Loads are happening, and the timer isn't running, so start it. + loading_animation_timer_.Start( + TimeDelta::FromMilliseconds(kLoadingAnimationFrameTimeMs), this, + &TabStrip::LoadingAnimationCallback); + } + } else { + if (loading_animation_timer_.IsRunning()) { + loading_animation_timer_.Stop(); + // Loads are now complete, update the state if a task was scheduled. + LoadingAnimationCallback(); + } + } +} + +/////////////////////////////////////////////////////////////////////////////// +// 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 (model_->ContainsIndex(index)) + model_->SelectTabContentsAt(index, true); +} + +void TabStrip::CloseTab(Tab* tab) { + int tab_index = GetIndexOfTab(tab); + if (model_->ContainsIndex(tab_index)) { + TabContents* contents = model_->GetTabContentsAt(tab_index); + if (contents) + UserMetrics::RecordAction(L"CloseTab_Mouse", contents->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 (model_->ContainsIndex(index)) + return model_->IsContextMenuCommandEnabled(index, command_id); + return false; +} + +void TabStrip::ExecuteCommandForTab( + TabStripModel::ContextMenuCommand command_id, Tab* tab) { + int index = GetIndexOfTab(tab); + if (model_->ContainsIndex(index)) + model_->ExecuteContextMenuCommand(index, command_id); +} + +void TabStrip::StartHighlightTabsForCommand( + TabStripModel::ContextMenuCommand command_id, Tab* tab) { + if (command_id == TabStripModel::CommandCloseTabsOpenedBy) { + int index = GetIndexOfTab(tab); + if (model_->ContainsIndex(index)) { + std::vector<int> indices = model_->GetIndexesOpenedBy(index); + std::vector<int>::const_iterator iter = indices.begin(); + for (; iter != indices.end(); ++iter) { + int current_index = *iter; + DCHECK(current_index >= 0 && current_index < GetTabCount()); + Tab* current_tab = GetTabAt(current_index); + current_tab->StartPulse(); + } + } + } else if (command_id == TabStripModel::CommandCloseTabsToRight) { + int index = GetIndexOfTab(tab); + if (model_->ContainsIndex(index)) { + for (int i = index + 1; i < GetTabCount(); ++i) { + Tab* current_tab = GetTabAt(i); + current_tab->StartPulse(); + } + } + } else if (command_id == TabStripModel::CommandCloseOtherTabs) { + for (int i = 0; i < GetTabCount(); ++i) { + Tab* current_tab = GetTabAt(i); + if (current_tab != tab) + current_tab->StartPulse(); + } + } +} + +void TabStrip::StopHighlightTabsForCommand( + TabStripModel::ContextMenuCommand command_id, Tab* tab) { + if (command_id == TabStripModel::CommandCloseTabsOpenedBy || + command_id == TabStripModel::CommandCloseTabsToRight || + command_id == TabStripModel::CommandCloseOtherTabs) { + // Just tell all Tabs to stop pulsing - it's safe. + StopAllHighlighting(); + } +} + +void TabStrip::StopAllHighlighting() { + for (int i = 0; i < GetTabCount(); ++i) + GetTabAt(i)->StopPulse(); +} + +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, 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_); + + 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; +} + +Tab* TabStrip::GetTabAtAdjustForAnimation(int index) const { + if (active_animation_.get() && + active_animation_->type() == TabAnimation::REMOVE && + index >= + static_cast<RemoveTabAnimation*>(active_animation_.get())->index()) { + index++; + } + return GetTabAt(index); +} + +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_) { + MessageLoopForUI::current()->AddObserver(this); + added_as_message_loop_observer_ = true; + } +} + +void TabStrip::RemoveMessageLoopObserver() { + if (added_as_message_loop_observer_) { + MessageLoopForUI::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), + true); + arrow_window->SetContentsView(arrow_view); +} + +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(); +} + +bool TabStrip::IsPointInTab(Tab* tab, const CPoint& point_in_tabstrip_coords) { + CPoint point_in_tab_coords(point_in_tabstrip_coords); + View::ConvertPointToView(this, tab, &point_in_tab_coords); + return tab->HitTest(point_in_tab_coords); +} + diff --git a/chrome/browser/views/tabs/tab_strip.h b/chrome/browser/views/tabs/tab_strip.h new file mode 100644 index 0000000..656dd12 --- /dev/null +++ b/chrome/browser/views/tabs/tab_strip.h @@ -0,0 +1,373 @@ +// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_VIEWS_TABS_TAB_STRIP_H__ +#define CHROME_BROWSER_VIEWS_TABS_TAB_STRIP_H__ + +#include "base/gfx/point.h" +#include "chrome/browser/tabs/tab_strip_model.h" +#include "chrome/browser/views/tabs/tab.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; + +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 MessageLoopForUI::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; + + // Returns true if the specified point (in TabStrip coordinates) is within a + // portion of the TabStrip that should be treated as the containing Window's + // titlebar for dragging purposes. + // TODO(beng): (Cleanup) should be const, but GetViewForPoint isn't, so fie! + bool PointIsWithinWindowCaption(const CPoint& point); + + // 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(); + + // Removes the drag source Tab from this TabStrip, and deletes it. + void DestroyDraggedSourceTab(Tab* tab); + + // 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); + virtual ChromeViews::View* GetViewForPoint(const CPoint& point); + virtual ChromeViews::View* GetViewForPoint(const CPoint& point, + bool can_create_floating); + + 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 StartHighlightTabsForCommand( + TabStripModel::ContextMenuCommand command_id, Tab* tab); + virtual void StopHighlightTabsForCommand( + TabStripModel::ContextMenuCommand command_id, Tab* tab); + virtual void StopAllHighlighting(); + 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); + + // 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. Take care in using this, you may + // need to use GetTabAtAdjustForAnimation. + Tab* GetTabAt(int index) const; + + // Returns the tab at the specified index. If a remove animation is on going + // and the index is >= the index of the tab being removed, the index is + // incremented. While a remove operation is on going the indices of the model + // do not line up with the indices of the view. This method adjusts the index + // accordingly. + // + // Use this instead of GetTabAt if the index comes from the model. + Tab* GetTabAtAdjustForAnimation(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; + + // Returns true if the specified point in TabStrip coords is within the + // hit-test region of the specified Tab. + bool IsPointInTab(Tab* tab, const CPoint& point_in_tabstrip_coords); + + // -- 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. + base::RepeatingTimer<TabStrip> 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_VIEWS_TABS_TAB_STRIP_H__ + |