diff options
Diffstat (limited to 'chrome/browser/gtk/tabs/tab_strip_gtk.cc')
-rw-r--r-- | chrome/browser/gtk/tabs/tab_strip_gtk.cc | 2049 |
1 files changed, 2049 insertions, 0 deletions
diff --git a/chrome/browser/gtk/tabs/tab_strip_gtk.cc b/chrome/browser/gtk/tabs/tab_strip_gtk.cc new file mode 100644 index 0000000..e8e4cdf --- /dev/null +++ b/chrome/browser/gtk/tabs/tab_strip_gtk.cc @@ -0,0 +1,2049 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/gtk/tabs/tab_strip_gtk.h" + +#include <algorithm> + +#include "app/gtk_dnd_util.h" +#include "app/resource_bundle.h" +#include "app/slide_animation.h" +#include "base/i18n/rtl.h" +#include "base/string_util.h" +#include "chrome/browser/autocomplete/autocomplete.h" +#include "chrome/browser/browser_theme_provider.h" +#include "chrome/browser/gtk/browser_window_gtk.h" +#include "chrome/browser/gtk/custom_button.h" +#include "chrome/browser/gtk/gtk_theme_provider.h" +#include "chrome/browser/gtk/gtk_util.h" +#include "chrome/browser/gtk/tabs/dragged_tab_controller_gtk.h" +#include "chrome/browser/profile.h" +#include "chrome/browser/tab_contents/tab_contents.h" +#include "chrome/common/notification_service.h" +#include "chrome/common/notification_type.h" +#include "gfx/gtk_util.h" +#include "gfx/point.h" +#include "grit/app_resources.h" +#include "grit/theme_resources.h" + +namespace { + +const int kDefaultAnimationDurationMs = 100; +const int kResizeLayoutAnimationDurationMs = 166; +const int kReorderAnimationDurationMs = 166; +const int kAnimateToBoundsDurationMs = 150; +const int kMiniTabAnimationDurationMs = 150; + +const int kNewTabButtonHOffset = -5; +const int kNewTabButtonVOffset = 5; + +// The delay between when the mouse leaves the tabstrip and the resize animation +// is started. +const int kResizeTabsTimeMs = 300; + +// The range outside of the tabstrip where the pointer must enter/leave to +// start/stop the resize animation. +const int kTabStripAnimationVSlop = 40; + +const int kHorizontalMoveThreshold = 16; // pixels + +// The horizontal offset from one tab to the next, which results in overlapping +// tabs. +const int kTabHOffset = -16; + +// A linux specific menu item for toggling window decorations. +const int kShowWindowDecorationsCommand = 200; + +// Size of the drop indicator. +static int drop_indicator_width; +static int drop_indicator_height; + +inline int Round(double x) { + return static_cast<int>(x + 0.5); +} + +// widget->allocation is not guaranteed to be set. After window creation, +// we pick up the normal bounds by connecting to the configure-event signal. +gfx::Rect GetInitialWidgetBounds(GtkWidget* widget) { + GtkRequisition request; + gtk_widget_size_request(widget, &request); + return gfx::Rect(0, 0, request.width, request.height); +} + +// Sort rectangles based on their x position. We don't care about y position +// so we don't bother breaking ties. +int CompareGdkRectangles(const void* p1, const void* p2) { + int p1_x = static_cast<const GdkRectangle*>(p1)->x; + int p2_x = static_cast<const GdkRectangle*>(p2)->x; + if (p1_x < p2_x) + return -1; + else if (p1_x == p2_x) + return 0; + return 1; +} + +bool GdkRectMatchesTabFavIconBounds(const GdkRectangle& gdk_rect, TabGtk* tab) { + gfx::Rect favicon_bounds = tab->favicon_bounds(); + return gdk_rect.x == favicon_bounds.x() + tab->x() && + gdk_rect.y == favicon_bounds.y() + tab->y() && + gdk_rect.width == favicon_bounds.width() && + gdk_rect.height == favicon_bounds.height(); +} + +} // namespace + +//////////////////////////////////////////////////////////////////////////////// +// +// TabAnimation +// +// A base class for all TabStrip animations. +// +class TabStripGtk::TabAnimation : public AnimationDelegate { + public: + friend class TabStripGtk; + + // Possible types of animation. + enum Type { + INSERT, + REMOVE, + MOVE, + RESIZE, + MINI, + MINI_MOVE + }; + + TabAnimation(TabStripGtk* 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(Tween::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(TabStripGtk* tabstrip, + TabStripGtk::TabAnimation* animation, + int index) { + TabGtk* tab = tabstrip->GetTabAt(index); + double tab_width; + if (tab->mini()) { + tab_width = TabGtk::GetMiniWidth(); + } else { + double unselected, selected; + tabstrip->GetCurrentTabWidths(&unselected, &selected); + 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); + } + + // Returns the gap before the tab at the specified index. Subclass if during + // an animation you need to insert a gap before a tab. + virtual double GetGapWidth(int index) { + return 0; + } + + 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, + int start_mini_count, + int end_mini_count) { + tabstrip_->GetDesiredTabWidths(start_tab_count, start_mini_count, + &start_unselected_width_, + &start_selected_width_); + double standard_tab_width = + static_cast<double>(TabRendererGtk::GetStandardSize().width()); + + if ((end_tab_count - start_tab_count) > 0 && + start_unselected_width_ < standard_tab_width) { + double minimum_tab_width = static_cast<double>( + TabRendererGtk::GetMinimumUnselectedSize().width()); + start_unselected_width_ -= minimum_tab_width / start_tab_count; + } + + tabstrip_->GenerateIdealBounds(); + tabstrip_->GetDesiredTabWidths(end_tab_count, end_mini_count, + &end_unselected_width_, + &end_selected_width_); + } + + TabStripGtk* 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_COPY_AND_ASSIGN(TabAnimation); +}; + +//////////////////////////////////////////////////////////////////////////////// + +// Handles insertion of a Tab at |index|. +class InsertTabAnimation : public TabStripGtk::TabAnimation { + public: + explicit InsertTabAnimation(TabStripGtk* tabstrip, int index) + : TabAnimation(tabstrip, INSERT), + index_(index) { + int tab_count = tabstrip->GetTabCount(); + int end_mini_count = tabstrip->GetMiniTabCount(); + int start_mini_count = end_mini_count; + if (index < end_mini_count) + start_mini_count--; + GenerateStartAndEndWidths(tab_count - 1, tab_count, start_mini_count, + end_mini_count); + } + virtual ~InsertTabAnimation() {} + + protected: + // Overridden from TabStripGtk::TabAnimation: + virtual double GetWidthForTab(int index) const { + if (index == index_) { + bool is_selected = tabstrip_->model()->selected_index() == index; + double start_width, target_width; + if (index < tabstrip_->GetMiniTabCount()) { + start_width = TabGtk::GetMinimumSelectedSize().width(); + target_width = TabGtk::GetMiniWidth(); + } else { + target_width = + is_selected ? end_unselected_width_ : end_selected_width_; + start_width = + is_selected ? TabGtk::GetMinimumSelectedSize().width() : + TabGtk::GetMinimumUnselectedSize().width(); + } + + double delta = target_width - start_width; + if (delta > 0) + return start_width + (delta * animation_.GetCurrentValue()); + + return start_width; + } + + if (tabstrip_->GetTabAt(index)->mini()) + return TabGtk::GetMiniWidth(); + + 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_COPY_AND_ASSIGN(InsertTabAnimation); +}; + +//////////////////////////////////////////////////////////////////////////////// + +// Handles removal of a Tab from |index| +class RemoveTabAnimation : public TabStripGtk::TabAnimation { + public: + RemoveTabAnimation(TabStripGtk* tabstrip, int index, TabContents* contents) + : TabAnimation(tabstrip, REMOVE), + index_(index) { + int tab_count = tabstrip->GetTabCount(); + int start_mini_count = tabstrip->GetMiniTabCount(); + int end_mini_count = start_mini_count; + if (index < start_mini_count) + end_mini_count--; + GenerateStartAndEndWidths(tab_count, tab_count - 1, start_mini_count, + end_mini_count); + // If the last non-mini-tab is being removed we force a layout on + // completion. This is necessary as the value returned by GetTabHOffset + // changes once the tab is actually removed (which happens at the end of + // the animation), and unless we layout GetTabHOffset won't be called after + // the removal. + // We do the same when the last mini-tab is being removed for the same + // reason. + set_layout_on_completion(start_mini_count > 0 && + (end_mini_count == 0 || + (start_mini_count == end_mini_count && + tab_count == start_mini_count + 1))); + } + + virtual ~RemoveTabAnimation() {} + + // Returns the index of the tab being removed. + int index() const { return index_; } + + protected: + // Overridden from TabStripGtk::TabAnimation: + virtual double GetWidthForTab(int index) const { + TabGtk* tab = tabstrip_->GetTabAt(index); + + if (index == index_) { + // The tab(s) being removed are gradually shrunken depending on the state + // of the animation. + if (tab->mini()) { + return animation_.CurrentValueBetween(TabGtk::GetMiniWidth(), + -kTabHOffset); + } + + // Removed animated Tabs are never selected. + double start_width = start_unselected_width_; + // Make sure target_width is at least abs(kTabHOffset), otherwise if + // less than kTabHOffset during layout tabs get negatively offset. + double target_width = + std::max(abs(kTabHOffset), + TabGtk::GetMinimumUnselectedSize().width() + kTabHOffset); + return animation_.CurrentValueBetween(start_width, target_width); + } + + if (tab->mini()) + return TabGtk::GetMiniWidth(); + + if (tabstrip_->available_width_for_tabs_ != -1 && + index_ != tabstrip_->GetTabCount() - 1) { + return TabStripGtk::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) { + tabstrip_->RemoveTabAt(index_); + TabStripGtk::TabAnimation::AnimationEnded(animation); + } + + private: + int index_; + + DISALLOW_COPY_AND_ASSIGN(RemoveTabAnimation); +}; + +//////////////////////////////////////////////////////////////////////////////// + +// Handles the movement of a Tab from one position to another. +class MoveTabAnimation : public TabStripGtk::TabAnimation { + public: + MoveTabAnimation(TabStripGtk* 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; + gfx::Rect bounds(Round(new_x), start_tab_a_bounds_.y(), tab_a_->width(), + tab_a_->height()); + tabstrip_->SetTabBounds(tab_a_, bounds); + + // 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; + bounds = gfx::Rect(Round(new_x), start_tab_b_bounds_.y(), tab_b_->width(), + tab_b_->height()); + tabstrip_->SetTabBounds(tab_b_, bounds); + } + + protected: + // Overridden from TabStripGtk::TabAnimation: + virtual int GetDuration() const { return kReorderAnimationDurationMs; } + + private: + // The two tabs being exchanged. + TabGtk* tab_a_; + TabGtk* tab_b_; + + // ...and their bounds. + gfx::Rect start_tab_a_bounds_; + gfx::Rect start_tab_b_bounds_; + + DISALLOW_COPY_AND_ASSIGN(MoveTabAnimation); +}; + +//////////////////////////////////////////////////////////////////////////////// + +// Handles the animated resize layout of the entire TabStrip from one width +// to another. +class ResizeLayoutAnimation : public TabStripGtk::TabAnimation { + public: + explicit ResizeLayoutAnimation(TabStripGtk* tabstrip) + : TabAnimation(tabstrip, RESIZE) { + int tab_count = tabstrip->GetTabCount(); + int mini_tab_count = tabstrip->GetMiniTabCount(); + GenerateStartAndEndWidths(tab_count, tab_count, mini_tab_count, + mini_tab_count); + InitStartState(); + } + virtual ~ResizeLayoutAnimation() {} + + // Overridden from AnimationDelegate: + virtual void AnimationEnded(const Animation* animation) { + tabstrip_->needs_resize_layout_ = false; + TabStripGtk::TabAnimation::AnimationEnded(animation); + } + + protected: + // Overridden from TabStripGtk::TabAnimation: + virtual int GetDuration() const { + return kResizeLayoutAnimationDurationMs; + } + + virtual double GetWidthForTab(int index) const { + TabGtk* tab = tabstrip_->GetTabAt(index); + + if (tab->mini()) + return TabGtk::GetMiniWidth(); + + if (tab->IsSelected()) { + return animation_.CurrentValueBetween(start_selected_width_, + end_selected_width_); + } + + return animation_.CurrentValueBetween(start_unselected_width_, + end_unselected_width_); + } + + 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) { + TabGtk* current_tab = tabstrip_->GetTabAt(i); + if (!current_tab->mini()) { + if (current_tab->IsSelected()) { + start_selected_width_ = current_tab->width(); + } else { + start_unselected_width_ = current_tab->width(); + } + } + } + } + + DISALLOW_COPY_AND_ASSIGN(ResizeLayoutAnimation); +}; + +// Handles a tabs mini-state changing while the tab does not change position +// in the model. +class MiniTabAnimation : public TabStripGtk::TabAnimation { + public: + explicit MiniTabAnimation(TabStripGtk* tabstrip, int index) + : TabAnimation(tabstrip, MINI), + index_(index) { + int tab_count = tabstrip->GetTabCount(); + int start_mini_count = tabstrip->GetMiniTabCount(); + int end_mini_count = start_mini_count; + if (tabstrip->GetTabAt(index)->mini()) + start_mini_count--; + else + start_mini_count++; + tabstrip_->GetTabAt(index)->set_animating_mini_change(true); + GenerateStartAndEndWidths(tab_count, tab_count, start_mini_count, + end_mini_count); + } + + protected: + // Overridden from TabStripGtk::TabAnimation: + virtual int GetDuration() const { + return kMiniTabAnimationDurationMs; + } + + virtual double GetWidthForTab(int index) const { + TabGtk* tab = tabstrip_->GetTabAt(index); + + if (index == index_) { + if (tab->mini()) { + return animation_.CurrentValueBetween( + start_selected_width_, + static_cast<double>(TabGtk::GetMiniWidth())); + } else { + return animation_.CurrentValueBetween( + static_cast<double>(TabGtk::GetMiniWidth()), + end_selected_width_); + } + } else if (tab->mini()) { + return TabGtk::GetMiniWidth(); + } + + if (tab->IsSelected()) { + return animation_.CurrentValueBetween(start_selected_width_, + end_selected_width_); + } + + return animation_.CurrentValueBetween(start_unselected_width_, + end_unselected_width_); + } + + private: + // Index of the tab whose mini-state changed. + int index_; + + DISALLOW_COPY_AND_ASSIGN(MiniTabAnimation); +}; + +//////////////////////////////////////////////////////////////////////////////// + +// Handles the animation when a tabs mini-state changes and the tab moves as a +// result. +class MiniMoveAnimation : public TabStripGtk::TabAnimation { + public: + explicit MiniMoveAnimation(TabStripGtk* tabstrip, + int from_index, + int to_index, + const gfx::Rect& start_bounds) + : TabAnimation(tabstrip, MINI_MOVE), + tab_(tabstrip->GetTabAt(to_index)), + start_bounds_(start_bounds), + from_index_(from_index), + to_index_(to_index) { + int tab_count = tabstrip->GetTabCount(); + int start_mini_count = tabstrip->GetMiniTabCount(); + int end_mini_count = start_mini_count; + if (tabstrip->GetTabAt(to_index)->mini()) + start_mini_count--; + else + start_mini_count++; + GenerateStartAndEndWidths(tab_count, tab_count, start_mini_count, + end_mini_count); + target_bounds_ = tabstrip->GetIdealBounds(to_index); + tab_->set_animating_mini_change(true); + } + + // Overridden from AnimationDelegate: + virtual void AnimationProgressed(const Animation* animation) { + // Do the normal layout. + TabAnimation::AnimationProgressed(animation); + + // Then special case the position of the tab being moved. + int x = animation_.CurrentValueBetween(start_bounds_.x(), + target_bounds_.x()); + int width = animation_.CurrentValueBetween(start_bounds_.width(), + target_bounds_.width()); + gfx::Rect tab_bounds(x, start_bounds_.y(), width, + start_bounds_.height()); + tabstrip_->SetTabBounds(tab_, tab_bounds); + } + + virtual void AnimationEnded(const Animation* animation) { + tabstrip_->needs_resize_layout_ = false; + TabStripGtk::TabAnimation::AnimationEnded(animation); + } + + virtual double GetGapWidth(int index) { + if (to_index_ < from_index_) { + // The tab was made mini. + if (index == to_index_) { + double current_size = + animation_.CurrentValueBetween(0, target_bounds_.width()); + if (current_size < -kTabHOffset) + return -(current_size + kTabHOffset); + } else if (index == from_index_ + 1) { + return animation_.CurrentValueBetween(start_bounds_.width(), 0); + } + } else { + // The tab was was made a normal tab. + if (index == from_index_) { + return animation_.CurrentValueBetween( + TabGtk::GetMiniWidth() + kTabHOffset, 0); + } + } + return 0; + } + + protected: + // Overridden from TabStripGtk::TabAnimation: + virtual int GetDuration() const { return kReorderAnimationDurationMs; } + + virtual double GetWidthForTab(int index) const { + TabGtk* tab = tabstrip_->GetTabAt(index); + + if (index == to_index_) + return animation_.CurrentValueBetween(0, target_bounds_.width()); + + if (tab->mini()) + return TabGtk::GetMiniWidth(); + + if (tab->IsSelected()) { + return animation_.CurrentValueBetween(start_selected_width_, + end_selected_width_); + } + + return animation_.CurrentValueBetween(start_unselected_width_, + end_unselected_width_); + } + + private: + // The tab being moved. + TabGtk* tab_; + + // Initial bounds of tab_. + gfx::Rect start_bounds_; + + // Target bounds. + gfx::Rect target_bounds_; + + // Start and end indices of the tab. + int from_index_; + int to_index_; + + DISALLOW_COPY_AND_ASSIGN(MiniMoveAnimation); +}; + +//////////////////////////////////////////////////////////////////////////////// +// TabStripGtk, public: + +// static +const int TabStripGtk::mini_to_non_mini_gap_ = 3; + +TabStripGtk::TabStripGtk(TabStripModel* model, BrowserWindowGtk* window) + : current_unselected_width_(TabGtk::GetStandardSize().width()), + current_selected_width_(TabGtk::GetStandardSize().width()), + available_width_for_tabs_(-1), + needs_resize_layout_(false), + tab_vertical_offset_(0), + model_(model), + window_(window), + theme_provider_(GtkThemeProvider::GetFrom(model->profile())), + resize_layout_factory_(this), + added_as_message_loop_observer_(false) { + theme_provider_->InitThemesFor(this); + registrar_.Add(this, NotificationType::BROWSER_THEME_CHANGED, + NotificationService::AllSources()); +} + +TabStripGtk::~TabStripGtk() { + model_->RemoveObserver(this); + tabstrip_.Destroy(); + + // Free any remaining tabs. This is needed to free the very last tab, + // because it is not animated on close. This also happens when all of the + // tabs are closed at once. + std::vector<TabData>::iterator iterator = tab_data_.begin(); + for (; iterator < tab_data_.end(); iterator++) { + delete iterator->tab; + } + + tab_data_.clear(); + + // Make sure we unhook ourselves as a message loop observer so that we don't + // crash in the case where the user closes the last tab in a window. + RemoveMessageLoopObserver(); +} + +void TabStripGtk::Init() { + model_->AddObserver(this); + + tabstrip_.Own(gtk_fixed_new()); + ViewIDUtil::SetID(tabstrip_.get(), VIEW_ID_TAB_STRIP); + // We want the tab strip to be horizontally shrinkable, so that the Chrome + // window can be resized freely. + gtk_widget_set_size_request(tabstrip_.get(), 0, + TabGtk::GetMinimumUnselectedSize().height()); + gtk_widget_set_app_paintable(tabstrip_.get(), TRUE); + gtk_drag_dest_set(tabstrip_.get(), GTK_DEST_DEFAULT_ALL, + NULL, 0, + static_cast<GdkDragAction>( + GDK_ACTION_COPY | GDK_ACTION_MOVE | GDK_ACTION_LINK)); + static const int targets[] = { gtk_dnd_util::TEXT_URI_LIST, + gtk_dnd_util::NETSCAPE_URL, + -1 }; + gtk_dnd_util::SetDestTargetList(tabstrip_.get(), targets); + + g_signal_connect(tabstrip_.get(), "expose-event", + G_CALLBACK(OnExposeThunk), this); + g_signal_connect(tabstrip_.get(), "size-allocate", + G_CALLBACK(OnSizeAllocateThunk), this); + g_signal_connect(tabstrip_.get(), "drag-motion", + G_CALLBACK(OnDragMotionThunk), this); + g_signal_connect(tabstrip_.get(), "drag-drop", + G_CALLBACK(OnDragDropThunk), this); + g_signal_connect(tabstrip_.get(), "drag-leave", + G_CALLBACK(OnDragLeaveThunk), this); + g_signal_connect(tabstrip_.get(), "drag-data-received", + G_CALLBACK(OnDragDataReceivedThunk), this); + + newtab_button_.reset(MakeNewTabButton()); + + gtk_widget_show_all(tabstrip_.get()); + + bounds_ = GetInitialWidgetBounds(tabstrip_.get()); + + if (drop_indicator_width == 0) { + // Direction doesn't matter, both images are the same size. + GdkPixbuf* drop_image = GetDropArrowImage(true); + drop_indicator_width = gdk_pixbuf_get_width(drop_image); + drop_indicator_height = gdk_pixbuf_get_height(drop_image); + } + + ViewIDUtil::SetDelegateForWidget(widget(), this); +} + +void TabStripGtk::Show() { + gtk_widget_show(tabstrip_.get()); +} + +void TabStripGtk::Hide() { + gtk_widget_hide(tabstrip_.get()); +} + +void TabStripGtk::CancelActiveDragSession() { + if (!IsDragSessionActive()) + return; + + drag_controller_->EndDrag(true); +} + +void TabStripGtk::Layout() { + // Called from: + // - window resize + // - animation completion + StopAnimation(); + + 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; + TabGtk* tab = GetTabAt(i); + tab->set_animating_mini_change(false); + tab->set_vertical_offset(tab_vertical_offset_); + SetTabBounds(tab, bounds); + tab_right = bounds.right(); + tab_right += GetTabHOffset(i + 1); + } + + LayoutNewTabButton(static_cast<double>(tab_right), current_unselected_width_); +} + +void TabStripGtk::SchedulePaint() { + gtk_widget_queue_draw(tabstrip_.get()); +} + +void TabStripGtk::SetBounds(const gfx::Rect& bounds) { + bounds_ = bounds; +} + +void TabStripGtk::UpdateLoadingAnimations() { + for (int i = 0, index = 0; i < GetTabCount(); ++i, ++index) { + TabGtk* current_tab = GetTabAt(i); + if (current_tab->closing()) { + --index; + } else { + TabRendererGtk::AnimationState state; + TabContents* contents = model_->GetTabContentsAt(index); + if (!contents || !contents->is_loading()) { + state = TabGtk::ANIMATION_NONE; + } else if (contents->waiting_for_response()) { + state = TabGtk::ANIMATION_WAITING; + } else { + state = TabGtk::ANIMATION_LOADING; + } + if (current_tab->ValidateLoadingAnimation(state)) { + // Queue the tab's icon area to be repainted. + gfx::Rect favicon_bounds = current_tab->favicon_bounds(); + gtk_widget_queue_draw_area(tabstrip_.get(), + favicon_bounds.x() + current_tab->x(), + favicon_bounds.y() + current_tab->y(), + favicon_bounds.width(), + favicon_bounds.height()); + } + } + } +} + +bool TabStripGtk::IsCompatibleWith(TabStripGtk* other) { + return model_->profile() == other->model()->profile(); +} + +bool TabStripGtk::IsAnimating() const { + return active_animation_.get() != NULL; +} + +void TabStripGtk::DestroyDragController() { + drag_controller_.reset(); +} + +void TabStripGtk::DestroyDraggedSourceTab(TabGtk* tab) { + // We could be running an animation that references this Tab. + StopAnimation(); + + // 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) { + if (!model_->closing_all()) + NOTREACHED() << "Leaving in an inconsistent state!"; + tab_data_.erase(it); + break; + } + } + + gtk_container_remove(GTK_CONTAINER(tabstrip_.get()), tab->widget()); + // If we delete the dragged source tab here, the DestroyDragWidget posted + // task will be run after the tab is deleted, leading to a crash. + MessageLoop::current()->DeleteSoon(FROM_HERE, 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 TabStripGtk::GetIdealBounds(int index) { + DCHECK(index >= 0 && index < GetTabCount()); + return tab_data_.at(index).ideal_bounds; +} + +void TabStripGtk::SetVerticalOffset(int offset) { + tab_vertical_offset_ = offset; + Layout(); +} + +gfx::Point TabStripGtk::GetTabStripOriginForWidget(GtkWidget* target) { + int x, y; + if (!gtk_widget_translate_coordinates(widget(), target, + -widget()->allocation.x, 0, &x, &y)) { + // If the tab strip isn't showing, give the coordinates relative to the + // toplevel instead. + if (!gtk_widget_translate_coordinates( + gtk_widget_get_toplevel(widget()), target, 0, 0, &x, &y)) { + NOTREACHED(); + } + } + if (GTK_WIDGET_NO_WINDOW(target)) { + x += target->allocation.x; + y += target->allocation.y; + } + return gfx::Point(x, y); +} + +//////////////////////////////////////////////////////////////////////////////// +// ViewIDUtil::Delegate implementation + +GtkWidget* TabStripGtk::GetWidgetForViewID(ViewID view_id) { + if (GetTabCount() > 0) { + if (view_id == VIEW_ID_TAB_LAST) { + return GetTabAt(GetTabCount() - 1)->widget(); + } 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)->widget(); + } else { + return NULL; + } + } + } + + return NULL; +} + +//////////////////////////////////////////////////////////////////////////////// +// TabStripGtk, TabStripModelObserver implementation: + +void TabStripGtk::TabInsertedAt(TabContents* contents, + int index, + bool foreground) { + DCHECK(contents); + DCHECK(index == TabStripModel::kNoTab || model_->ContainsIndex(index)); + + StopAnimation(); + + bool contains_tab = false; + TabGtk* 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(TabRendererGtk::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; + } + } + + if (!tab) + tab = new TabGtk(this); + + // Only insert if we're not already in the list. + if (!contains_tab) { + TabData d = { tab, gfx::Rect() }; + tab_data_.insert(tab_data_.begin() + index, d); + tab->UpdateData(contents, model_->IsPhantomTab(index), + model_->IsAppTab(index), false); + } + tab->set_mini(model_->IsMiniTab(index)); + tab->set_app(model_->IsAppTab(index)); + tab->SetBlocked(model_->IsTabBlocked(index)); + + if (gtk_widget_get_parent(tab->widget()) != tabstrip_.get()) + gtk_fixed_put(GTK_FIXED(tabstrip_.get()), tab->widget(), 0, 0); + + // Don't animate the first tab; it looks weird. + if (GetTabCount() > 1) { + StartInsertTabAnimation(index); + // We added the tab at 0x0, we need to force an animation step otherwise + // if GTK paints before the animation event the tab is painted at 0x0 + // which is most likely not where it should be positioned. + active_animation_->AnimationProgressed(NULL); + } else { + Layout(); + } +} + +void TabStripGtk::TabDetachedAt(TabContents* contents, int index) { + 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 TabStripGtk::TabSelectedAt(TabContents* old_contents, + TabContents* new_contents, + int index, + bool user_gesture) { + DCHECK(index >= 0 && index < static_cast<int>(GetTabCount())); + + // 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() && (!needs_resize_layout_ || tiny_tabs)) + Layout(); + + GetTabAt(index)->SchedulePaint(); + + int old_index = model_->GetIndexOfTabContents(old_contents); + if (old_index >= 0) { + GetTabAt(old_index)->SchedulePaint(); + GetTabAt(old_index)->StopMiniTabTitleAnimation(); + } +} + +void TabStripGtk::TabMoved(TabContents* contents, + int from_index, + int to_index) { + gfx::Rect start_bounds = GetIdealBounds(from_index); + TabGtk* tab = GetTabAt(from_index); + tab_data_.erase(tab_data_.begin() + from_index); + TabData data = {tab, gfx::Rect()}; + tab->set_mini(model_->IsMiniTab(to_index)); + tab->SetBlocked(model_->IsTabBlocked(to_index)); + tab_data_.insert(tab_data_.begin() + to_index, data); + if (tab->phantom() != model_->IsPhantomTab(to_index)) + tab->set_phantom(!tab->phantom()); + GenerateIdealBounds(); + StartMoveTabAnimation(from_index, to_index); +} + +void TabStripGtk::TabChangedAt(TabContents* contents, int index, + TabChangeType change_type) { + // Index is in terms of the model. Need to make sure we adjust that index in + // case we have an animation going. + TabGtk* tab = GetTabAtAdjustForAnimation(index); + if (change_type == TITLE_NOT_LOADING) { + if (tab->mini() && !tab->IsSelected()) + tab->StartMiniTabTitleAnimation(); + // We'll receive another notification of the change asynchronously. + return; + } + tab->UpdateData(contents, + model_->IsPhantomTab(index), + model_->IsAppTab(index), + change_type == LOADING_ONLY); + tab->UpdateFromModel(); +} + +void TabStripGtk::TabReplacedAt(TabContents* old_contents, + TabContents* new_contents, + int index) { + TabChangedAt(new_contents, index, ALL); +} + +void TabStripGtk::TabMiniStateChanged(TabContents* contents, int index) { + GetTabAt(index)->set_mini(model_->IsMiniTab(index)); + // Don't animate if the window isn't visible yet. The window won't be visible + // when dragging a mini-tab to a new window. + if (window_ && window_->window() && + GTK_WIDGET_VISIBLE(GTK_WIDGET(window_->window()))) { + StartMiniTabAnimation(index); + } else { + Layout(); + } +} + +void TabStripGtk::TabBlockedStateChanged(TabContents* contents, int index) { + GetTabAt(index)->SetBlocked(model_->IsTabBlocked(index)); +} + +//////////////////////////////////////////////////////////////////////////////// +// TabStripGtk, TabGtk::TabDelegate implementation: + +bool TabStripGtk::IsTabSelected(const TabGtk* tab) const { + if (tab->closing()) + return false; + + return GetIndexOfTab(tab) == model_->selected_index(); +} + +bool TabStripGtk::IsTabDetached(const TabGtk* tab) const { + if (drag_controller_.get()) + return drag_controller_->IsTabDetached(tab); + return false; +} + +void TabStripGtk::GetCurrentTabWidths(double* unselected_width, + double* selected_width) const { + *unselected_width = current_unselected_width_; + *selected_width = current_selected_width_; +} + +bool TabStripGtk::IsTabPinned(const TabGtk* tab) const { + if (tab->closing()) + return false; + + return model_->IsTabPinned(GetIndexOfTab(tab)); +} + +void TabStripGtk::SelectTab(TabGtk* tab) { + int index = GetIndexOfTab(tab); + if (model_->ContainsIndex(index)) + model_->SelectTabContentsAt(index, true); +} + +void TabStripGtk::CloseTab(TabGtk* tab) { + int tab_index = GetIndexOfTab(tab); + if (model_->ContainsIndex(tab_index)) { + TabGtk* 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); + needs_resize_layout_ = true; + // We hook into the message loop in order to receive mouse move events when + // the mouse is outside of the tabstrip. We unhook once the resize layout + // animation is started. + AddMessageLoopObserver(); + model_->CloseTabContentsAt(tab_index, + TabStripModel::CLOSE_USER_GESTURE | + TabStripModel::CLOSE_CREATE_HISTORICAL_TAB); + } +} + +bool TabStripGtk::IsCommandEnabledForTab( + TabStripModel::ContextMenuCommand command_id, const TabGtk* tab) const { + int index = GetIndexOfTab(tab); + if (model_->ContainsIndex(index)) + return model_->IsContextMenuCommandEnabled(index, command_id); + return false; +} + +void TabStripGtk::ExecuteCommandForTab( + TabStripModel::ContextMenuCommand command_id, TabGtk* tab) { + int index = GetIndexOfTab(tab); + if (model_->ContainsIndex(index)) + model_->ExecuteContextMenuCommand(index, command_id); +} + +void TabStripGtk::StartHighlightTabsForCommand( + TabStripModel::ContextMenuCommand command_id, TabGtk* tab) { + if (command_id == TabStripModel::CommandCloseOtherTabs || + command_id == TabStripModel::CommandCloseTabsToRight) { + NOTIMPLEMENTED(); + } +} + +void TabStripGtk::StopHighlightTabsForCommand( + TabStripModel::ContextMenuCommand command_id, TabGtk* tab) { + if (command_id == TabStripModel::CommandCloseTabsToRight || + command_id == TabStripModel::CommandCloseOtherTabs) { + // Just tell all Tabs to stop pulsing - it's safe. + StopAllHighlighting(); + } +} + +void TabStripGtk::StopAllHighlighting() { + // TODO(jhawkins): Hook up animations. + NOTIMPLEMENTED(); +} + +void TabStripGtk::MaybeStartDrag(TabGtk* tab, const gfx::Point& point) { + // Don't accidentally start any drag operations during animations if the + // mouse is down. + if (IsAnimating() || tab->closing() || !HasAvailableDragActions()) + return; + + drag_controller_.reset(new DraggedTabControllerGtk(tab, this)); + drag_controller_->CaptureDragInfo(point); +} + +void TabStripGtk::ContinueDrag(GdkDragContext* context) { + // 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(); +} + +bool TabStripGtk::EndDrag(bool canceled) { + return drag_controller_.get() ? drag_controller_->EndDrag(canceled) : false; +} + +bool TabStripGtk::HasAvailableDragActions() const { + return model_->delegate()->GetDragActions() != 0; +} + +ThemeProvider* TabStripGtk::GetThemeProvider() { + return theme_provider_; +} + +/////////////////////////////////////////////////////////////////////////////// +// TabStripGtk, MessageLoop::Observer implementation: + +void TabStripGtk::WillProcessEvent(GdkEvent* event) { + // Nothing to do. +} + +void TabStripGtk::DidProcessEvent(GdkEvent* event) { + switch (event->type) { + case GDK_MOTION_NOTIFY: + case GDK_LEAVE_NOTIFY: + HandleGlobalMouseMoveEvent(); + break; + default: + break; + } +} + +void TabStripGtk::Observe(NotificationType type, + const NotificationSource& source, + const NotificationDetails& details) { + if (type == NotificationType::BROWSER_THEME_CHANGED) { + TabRendererGtk::SetSelectedTitleColor(theme_provider_->GetColor( + BrowserThemeProvider::COLOR_TAB_TEXT)); + TabRendererGtk::SetUnselectedTitleColor(theme_provider_->GetColor( + BrowserThemeProvider::COLOR_BACKGROUND_TAB_TEXT)); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// TabStripGtk, private: + +int TabStripGtk::GetTabCount() const { + return static_cast<int>(tab_data_.size()); +} + +int TabStripGtk::GetMiniTabCount() const { + int mini_count = 0; + for (size_t i = 0; i < tab_data_.size(); ++i) { + if (tab_data_[i].tab->mini()) + mini_count++; + else + return mini_count; + } + return mini_count; +} + +int TabStripGtk::GetAvailableWidthForTabs(TabGtk* last_tab) const { + if (!base::i18n::IsRTL()) + return last_tab->x() - bounds_.x() + last_tab->width(); + else + return bounds_.width() - last_tab->x(); +} + +int TabStripGtk::GetIndexOfTab(const TabGtk* tab) const { + for (int i = 0, index = 0; i < GetTabCount(); ++i, ++index) { + TabGtk* current_tab = GetTabAt(i); + if (current_tab->closing()) { + --index; + } else if (current_tab == tab) { + return index; + } + } + return -1; +} + +TabGtk* TabStripGtk::GetTabAt(int index) const { + DCHECK_GE(index, 0); + DCHECK_LT(index, GetTabCount()); + return tab_data_.at(index).tab; +} + +TabGtk* TabStripGtk::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); +} + +void TabStripGtk::RemoveTabAt(int index) { + TabGtk* removed = tab_data_.at(index).tab; + + // Remove the Tab from the TabStrip's list. + tab_data_.erase(tab_data_.begin() + index); + + if (!IsDragSessionActive() || !drag_controller_->IsDragSourceTab(removed)) { + gtk_container_remove(GTK_CONTAINER(tabstrip_.get()), removed->widget()); + delete removed; + } +} + +void TabStripGtk::HandleGlobalMouseMoveEvent() { + 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( + &TabStripGtk::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(); + } +} + +void TabStripGtk::GenerateIdealBounds() { + int tab_count = GetTabCount(); + double unselected, selected; + GetDesiredTabWidths(tab_count, GetMiniTabCount(), &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 = TabGtk::GetStandardSize().height(); + double tab_x = tab_start_x(); + for (int i = 0; i < tab_count; ++i) { + TabGtk* tab = GetTabAt(i); + double tab_width = unselected; + if (tab->mini()) + tab_width = TabGtk::GetMiniWidth(); + else 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 + GetTabHOffset(i + 1); + } +} + +void TabStripGtk::LayoutNewTabButton(double last_tab_right, + double unselected_width) { + gfx::Rect bounds(0, kNewTabButtonVOffset, + newtab_button_->width(), newtab_button_->height()); + int delta = abs(Round(unselected_width) - TabGtk::GetStandardSize().width()); + if (delta > 1 && !needs_resize_layout_) { + // 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. + bounds.set_x(bounds_.width() - newtab_button_->width()); + } else { + bounds.set_x(Round(last_tab_right - kTabHOffset) + kNewTabButtonHOffset); + } + bounds.set_x(gtk_util::MirroredLeftPointForRect(tabstrip_.get(), bounds)); + + gtk_fixed_move(GTK_FIXED(tabstrip_.get()), newtab_button_->widget(), + bounds.x(), bounds.y()); +} + +void TabStripGtk::GetDesiredTabWidths(int tab_count, + int mini_tab_count, + double* unselected_width, + double* selected_width) const { + DCHECK(tab_count >= 0 && mini_tab_count >= 0 && mini_tab_count <= tab_count); + const double min_unselected_width = + TabGtk::GetMinimumUnselectedSize().width(); + const double min_selected_width = + TabGtk::GetMinimumSelectedSize().width(); + + *unselected_width = min_unselected_width; + *selected_width = min_selected_width; + + if (tab_count == 0) { + // Return immediately to avoid divide-by-zero below. + return; + } + + // Determine how much space we can actually allocate to tabs. + int available_width = tabstrip_->allocation.width; + if (available_width_for_tabs_ < 0) { + available_width = bounds_.width(); + available_width -= + (kNewTabButtonHOffset + newtab_button_->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_; + } + + if (mini_tab_count > 0) { + available_width -= mini_tab_count * (TabGtk::GetMiniWidth() + kTabHOffset); + tab_count -= mini_tab_count; + if (tab_count == 0) { + *selected_width = *unselected_width = TabGtk::GetStandardSize().width(); + return; + } + // Account for gap between the last mini-tab and first normal tab. + available_width -= mini_to_non_mini_gap_; + } + + // 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>(TabGtk::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)) { + double calc_width = + static_cast<double>( + available_width - total_offset - min_selected_width) / + static_cast<double>(tab_count - 1); + *unselected_width = std::max(calc_width, min_unselected_width); + } else if ((min_unselected_width > min_selected_width) && + (desired_tab_width < min_unselected_width)) { + *selected_width = std::max(available_width - total_offset - + (min_unselected_width * (tab_count - 1)), min_selected_width); + } + } +} + +int TabStripGtk::GetTabHOffset(int tab_index) { + if (tab_index < GetTabCount() && GetTabAt(tab_index - 1)->mini() && + !GetTabAt(tab_index)->mini()) { + return mini_to_non_mini_gap_ + kTabHOffset; + } + return kTabHOffset; +} + +int TabStripGtk::tab_start_x() const { + return 0; +} + +void TabStripGtk::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; + int mini_tab_count = GetMiniTabCount(); + if (mini_tab_count == GetTabCount()) { + // Only mini tabs, we know the tab widths won't have changed (all mini-tabs + // have the same width), so there is nothing to do. + return; + } + TabGtk* first_tab = GetTabAt(mini_tab_count); + double unselected, selected; + GetDesiredTabWidths(GetTabCount(), mini_tab_count, &unselected, &selected); + int w = Round(first_tab->IsSelected() ? selected : unselected); + + // We only want to run the animation if we're not already at the desired + // size. + if (abs(first_tab->width() - w) > 1) + StartResizeLayoutAnimation(); +} + +bool TabStripGtk::IsCursorInTabStripZone() const { + gfx::Point tabstrip_topleft; + gtk_util::ConvertWidgetPointToScreen(tabstrip_.get(), &tabstrip_topleft); + + gfx::Rect bds = bounds(); + bds.set_origin(tabstrip_topleft); + bds.set_height(bds.height() + kTabStripAnimationVSlop); + + GdkScreen* screen = gdk_screen_get_default(); + GdkDisplay* display = gdk_screen_get_display(screen); + gint x, y; + gdk_display_get_pointer(display, NULL, &x, &y, NULL); + gfx::Point cursor_point(x, y); + + return bds.Contains(cursor_point); +} + +void TabStripGtk::AddMessageLoopObserver() { + if (!added_as_message_loop_observer_) { + MessageLoopForUI::current()->AddObserver(this); + added_as_message_loop_observer_ = true; + } +} + +void TabStripGtk::RemoveMessageLoopObserver() { + if (added_as_message_loop_observer_) { + MessageLoopForUI::current()->RemoveObserver(this); + added_as_message_loop_observer_ = false; + } +} + +gfx::Rect TabStripGtk::GetDropBounds(int drop_index, + bool drop_before, + bool* is_beneath) { + DCHECK_NE(drop_index, -1); + int center_x; + if (drop_index < GetTabCount()) { + TabGtk* tab = GetTabAt(drop_index); + gfx::Rect bounds = tab->GetNonMirroredBounds(tabstrip_.get()); + // TODO(sky): update these for pinned tabs. + if (drop_before) + center_x = bounds.x() - (kTabHOffset / 2); + else + center_x = bounds.x() + (bounds.width() / 2); + } else { + TabGtk* last_tab = GetTabAt(drop_index - 1); + gfx::Rect bounds = last_tab->GetNonMirroredBounds(tabstrip_.get()); + center_x = bounds.x() + bounds.width() + (kTabHOffset / 2); + } + + center_x = gtk_util::MirroredXCoordinate(tabstrip_.get(), center_x); + + // Determine the screen bounds. + gfx::Point drop_loc(center_x - drop_indicator_width / 2, + -drop_indicator_height); + gtk_util::ConvertWidgetPointToScreen(tabstrip_.get(), &drop_loc); + gfx::Rect drop_bounds(drop_loc.x(), drop_loc.y(), drop_indicator_width, + drop_indicator_height); + + // TODO(jhawkins): We always display the arrow underneath the tab because we + // don't have custom frame support yet. + *is_beneath = true; + if (*is_beneath) + drop_bounds.Offset(0, drop_bounds.height() + bounds().height()); + + return drop_bounds; +} + +void TabStripGtk::UpdateDropIndex(GdkDragContext* context, gint x, gint y) { + // 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. + x = gtk_util::MirroredXCoordinate(tabstrip_.get(), x); + // We don't allow replacing the urls of mini-tabs. + for (int i = GetMiniTabCount(); i < GetTabCount(); ++i) { + TabGtk* tab = GetTabAt(i); + gfx::Rect bounds = tab->GetNonMirroredBounds(tabstrip_.get()); + const int tab_max_x = bounds.x() + bounds.width(); + const int hot_width = bounds.width() / 3; + if (x < tab_max_x) { + if (x < bounds.x() + 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 TabStripGtk::SetDropIndex(int index, bool drop_before) { + 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 { + if (!GTK_IS_WIDGET(drop_info_->container)) { + drop_info_->CreateContainer(); + } else if (drop_info_->drop_index == index && + drop_info_->drop_before == drop_before) { + return; + } + + 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_->drop_arrow= GetDropArrowImage(drop_info_->point_down); + } + } + + gtk_window_move(GTK_WINDOW(drop_info_->container), + drop_bounds.x(), drop_bounds.y()); + gtk_window_resize(GTK_WINDOW(drop_info_->container), + drop_bounds.width(), drop_bounds.height()); +} + +bool TabStripGtk::CompleteDrop(guchar* data) { + if (!drop_info_.get()) + return false; + + const int drop_index = drop_info_->drop_index; + const bool drop_before = drop_info_->drop_before; + + // Destroy the drop indicator. + drop_info_.reset(); + + std::string url_string(reinterpret_cast<char*>(data)); + GURL url(url_string.substr(0, url_string.find_first_of('\n'))); + if (!url.is_valid()) + return false; + + if (drop_before) { + // Insert a new tab. + TabContents* contents = + model_->delegate()->CreateTabContentsForURL( + url, GURL(), model_->profile(), PageTransition::TYPED, false, + NULL); + model_->AddTabContents(contents, drop_index, PageTransition::GENERATED, + TabStripModel::ADD_SELECTED); + } else { + model_->GetTabContentsAt(drop_index)->controller().LoadURL( + url, GURL(), PageTransition::GENERATED); + model_->SelectTabContentsAt(drop_index, true); + } + + return true; +} + +// static +GdkPixbuf* TabStripGtk::GetDropArrowImage(bool is_down) { + return ResourceBundle::GetSharedInstance().GetPixbufNamed( + is_down ? IDR_TAB_DROP_DOWN : IDR_TAB_DROP_UP); +} + +// TabStripGtk::DropInfo ------------------------------------------------------- + +TabStripGtk::DropInfo::DropInfo(int drop_index, bool drop_before, + bool point_down) + : drop_index(drop_index), + drop_before(drop_before), + point_down(point_down) { + CreateContainer(); + drop_arrow = GetDropArrowImage(point_down); +} + +TabStripGtk::DropInfo::~DropInfo() { + DestroyContainer(); +} + +gboolean TabStripGtk::DropInfo::OnExposeEvent(GtkWidget* widget, + GdkEventExpose* event) { + if (gtk_util::IsScreenComposited()) { + SetContainerTransparency(); + } else { + SetContainerShapeMask(); + } + + gdk_pixbuf_render_to_drawable(drop_arrow, + container->window, + 0, 0, 0, + 0, 0, + drop_indicator_width, + drop_indicator_height, + GDK_RGB_DITHER_NONE, 0, 0); + + return FALSE; +} + +// Sets the color map of the container window to allow the window to be +// transparent. +void TabStripGtk::DropInfo::SetContainerColorMap() { + GdkScreen* screen = gtk_widget_get_screen(container); + GdkColormap* colormap = gdk_screen_get_rgba_colormap(screen); + + // If rgba is not available, use rgb instead. + if (!colormap) + colormap = gdk_screen_get_rgb_colormap(screen); + + gtk_widget_set_colormap(container, colormap); +} + +// Sets full transparency for the container window. This is used if +// compositing is available for the screen. +void TabStripGtk::DropInfo::SetContainerTransparency() { + cairo_t* cairo_context = gdk_cairo_create(container->window); + if (!cairo_context) + return; + + // Make the background of the dragged tab window fully transparent. All of + // the content of the window (child widgets) will be completely opaque. + + cairo_scale(cairo_context, static_cast<double>(drop_indicator_width), + static_cast<double>(drop_indicator_height)); + cairo_set_source_rgba(cairo_context, 1.0f, 1.0f, 1.0f, 0.0f); + cairo_set_operator(cairo_context, CAIRO_OPERATOR_SOURCE); + cairo_paint(cairo_context); + cairo_destroy(cairo_context); +} + +// Sets the shape mask for the container window to emulate a transparent +// container window. This is used if compositing is not available for the +// screen. +void TabStripGtk::DropInfo::SetContainerShapeMask() { + // Create a 1bpp bitmap the size of |container|. + GdkPixmap* pixmap = gdk_pixmap_new(NULL, + drop_indicator_width, + drop_indicator_height, 1); + cairo_t* cairo_context = gdk_cairo_create(GDK_DRAWABLE(pixmap)); + + // Set the transparency. + cairo_set_source_rgba(cairo_context, 1, 1, 1, 0); + + // Blit the rendered bitmap into a pixmap. Any pixel set in the pixmap will + // be opaque in the container window. + cairo_set_operator(cairo_context, CAIRO_OPERATOR_SOURCE); + gdk_cairo_set_source_pixbuf(cairo_context, drop_arrow, 0, 0); + cairo_paint(cairo_context); + cairo_destroy(cairo_context); + + // Set the shape mask. + gdk_window_shape_combine_mask(container->window, pixmap, 0, 0); + g_object_unref(pixmap); +} + +void TabStripGtk::DropInfo::CreateContainer() { + container = gtk_window_new(GTK_WINDOW_POPUP); + SetContainerColorMap(); + gtk_widget_set_app_paintable(container, TRUE); + g_signal_connect(container, "expose-event", + G_CALLBACK(OnExposeEventThunk), this); + gtk_widget_add_events(container, GDK_STRUCTURE_MASK); + gtk_window_move(GTK_WINDOW(container), 0, 0); + gtk_window_resize(GTK_WINDOW(container), + drop_indicator_width, drop_indicator_height); + gtk_widget_show_all(container); +} + +void TabStripGtk::DropInfo::DestroyContainer() { + if (GTK_IS_WIDGET(container)) + gtk_widget_destroy(container); +} + +void TabStripGtk::StopAnimation() { + if (active_animation_.get()) + active_animation_->Stop(); +} + +// Called from: +// - animation tick +void TabStripGtk::AnimationLayout(double unselected_width) { + int tab_height = TabGtk::GetStandardSize().height(); + double tab_x = tab_start_x(); + for (int i = 0; i < GetTabCount(); ++i) { + TabAnimation* animation = active_animation_.get(); + if (animation) + tab_x += animation->GetGapWidth(i); + double tab_width = TabAnimation::GetCurrentTabWidth(this, animation, i); + double end_of_tab = tab_x + tab_width; + int rounded_tab_x = Round(tab_x); + TabGtk* tab = GetTabAt(i); + gfx::Rect bounds(rounded_tab_x, 0, Round(end_of_tab) - rounded_tab_x, + tab_height); + SetTabBounds(tab, bounds); + tab_x = end_of_tab + GetTabHOffset(i + 1); + } + LayoutNewTabButton(tab_x, unselected_width); +} + +void TabStripGtk::StartInsertTabAnimation(int index) { + // The TabStrip can now use its entire width to lay out Tabs. + available_width_for_tabs_ = -1; + StopAnimation(); + active_animation_.reset(new InsertTabAnimation(this, index)); + active_animation_->Start(); +} + +void TabStripGtk::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 TabStripGtk::StartMoveTabAnimation(int from_index, int to_index) { + StopAnimation(); + active_animation_.reset(new MoveTabAnimation(this, from_index, to_index)); + active_animation_->Start(); +} + +void TabStripGtk::StartResizeLayoutAnimation() { + StopAnimation(); + active_animation_.reset(new ResizeLayoutAnimation(this)); + active_animation_->Start(); +} + +void TabStripGtk::StartMiniTabAnimation(int index) { + StopAnimation(); + active_animation_.reset(new MiniTabAnimation(this, index)); + active_animation_->Start(); +} + +void TabStripGtk::StartMiniMoveTabAnimation(int from_index, + int to_index, + const gfx::Rect& start_bounds) { + StopAnimation(); + active_animation_.reset( + new MiniMoveAnimation(this, from_index, to_index, start_bounds)); + active_animation_->Start(); +} + +void TabStripGtk::FinishAnimation(TabStripGtk::TabAnimation* animation, + bool layout) { + active_animation_.reset(NULL); + + // Reset the animation state of each tab. + for (int i = 0, count = GetTabCount(); i < count; ++i) + GetTabAt(i)->set_animating_mini_change(false); + + if (layout) + Layout(); +} + +gboolean TabStripGtk::OnExpose(GtkWidget* widget, GdkEventExpose* event) { + if (gdk_region_empty(event->region)) + return TRUE; + + // If we're only repainting favicons, optimize the paint path and only draw + // the favicons. + GdkRectangle* rects; + gint num_rects; + gdk_region_get_rectangles(event->region, &rects, &num_rects); + qsort(rects, num_rects, sizeof(GdkRectangle), CompareGdkRectangles); + std::vector<int> tabs_to_repaint; + if (!IsDragSessionActive() && + CanPaintOnlyFavIcons(rects, num_rects, &tabs_to_repaint)) { + PaintOnlyFavIcons(event, tabs_to_repaint); + g_free(rects); + return TRUE; + } + g_free(rects); + + // TODO(jhawkins): Ideally we'd like to only draw what's needed in the damage + // rect, but the tab widgets overlap each other, and painting on one widget + // will cause an expose-event to be sent to the widgets underneath. The + // underlying widget does not need to be redrawn as we control the order of + // expose-events. Currently we hack it to redraw the entire tabstrip. We + // could change the damage rect to just contain the tabs + the new tab button. + event->area.x = 0; + event->area.y = 0; + event->area.width = bounds_.width(); + event->area.height = bounds_.height(); + gdk_region_union_with_rect(event->region, &event->area); + + // Paint the New Tab button. + gtk_container_propagate_expose(GTK_CONTAINER(tabstrip_.get()), + newtab_button_->widget(), event); + + // Paint the tabs in reverse order, so they stack to the left. + TabGtk* selected_tab = NULL; + int tab_count = GetTabCount(); + for (int i = tab_count - 1; i >= 0; --i) { + TabGtk* 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()) { + gtk_container_propagate_expose(GTK_CONTAINER(tabstrip_.get()), + tab->widget(), event); + } else { + selected_tab = tab; + } + } + + // Paint the selected tab last, so it overlaps all the others. + if (selected_tab) { + gtk_container_propagate_expose(GTK_CONTAINER(tabstrip_.get()), + selected_tab->widget(), event); + } + + return TRUE; +} + +void TabStripGtk::OnSizeAllocate(GtkWidget* widget, GtkAllocation* allocation) { + gfx::Rect bounds = gfx::Rect(allocation->x, allocation->y, + allocation->width, allocation->height); + + // Nothing to do if the bounds are the same. If we don't catch this, we'll + // get an infinite loop of size-allocate signals. + if (bounds_ == bounds) + return; + + SetBounds(bounds); + + // No tabs, nothing to layout. This happens when a browser window is created + // and shown before tabs are added (as in a popup window). + if (GetTabCount() == 0) + return; + + // Do a regular layout on the first configure-event so we don't animate + // the first tab. + // TODO(jhawkins): Windows resizes the layout tabs continuously during + // a resize. I need to investigate which signal to watch in order to + // reproduce this behavior. + if (GetTabCount() == 1) + Layout(); + else + ResizeLayoutTabs(); +} + +gboolean TabStripGtk::OnDragMotion(GtkWidget* widget, GdkDragContext* context, + gint x, gint y, guint time) { + UpdateDropIndex(context, x, y); + return TRUE; +} + +gboolean TabStripGtk::OnDragDrop(GtkWidget* widget, GdkDragContext* context, + gint x, gint y, guint time) { + if (!drop_info_.get()) + return FALSE; + + GdkAtom target = gtk_drag_dest_find_target(widget, context, NULL); + if (target != GDK_NONE) + gtk_drag_finish(context, FALSE, FALSE, time); + else + gtk_drag_get_data(widget, context, target, time); + + return TRUE; +} + +gboolean TabStripGtk::OnDragLeave(GtkWidget* widget, GdkDragContext* context, + guint time) { + // Destroy the drop indicator. + drop_info_->DestroyContainer(); + return FALSE; +} + +gboolean TabStripGtk::OnDragDataReceived(GtkWidget* widget, + GdkDragContext* context, + gint x, gint y, + GtkSelectionData* data, + guint info, guint time) { + bool success = false; + + if (info == gtk_dnd_util::TEXT_URI_LIST || + info == gtk_dnd_util::NETSCAPE_URL) { + success = CompleteDrop(data->data); + } + + gtk_drag_finish(context, success, success, time); + return TRUE; +} + +void TabStripGtk::OnNewTabClicked(GtkWidget* widget) { + GdkEvent* event = gtk_get_current_event(); + DCHECK_EQ(event->type, GDK_BUTTON_RELEASE); + int mouse_button = event->button.button; + gdk_event_free(event); + + switch (mouse_button) { + case 1: + model_->delegate()->AddBlankTab(true); + break; + case 2: { + // On middle-click, try to parse the PRIMARY selection as a URL and load + // it instead of creating a blank page. + GURL url; + if (!gtk_util::URLFromPrimarySelection(model_->profile(), &url)) + return; + + TabContents* contents = + model_->delegate()->CreateTabContentsForURL( + url, + GURL(), // referrer + model_->profile(), + PageTransition::TYPED, + false, // defer_load + NULL); // instance + model_->AddTabContents( + contents, + -1, // index + PageTransition::TYPED, + TabStripModel::ADD_SELECTED); + break; + } + default: + NOTREACHED() << "Got click on new tab button with unhandled mouse " + << "button " << mouse_button; + } +} + +void TabStripGtk::SetTabBounds(TabGtk* tab, const gfx::Rect& bounds) { + gfx::Rect bds = bounds; + bds.set_x(gtk_util::MirroredLeftPointForRect(tabstrip_.get(), bounds)); + tab->SetBounds(bds); + gtk_fixed_move(GTK_FIXED(tabstrip_.get()), tab->widget(), + bds.x(), bds.y()); +} + +bool TabStripGtk::CanPaintOnlyFavIcons(const GdkRectangle* rects, + int num_rects, std::vector<int>* tabs_to_paint) { + // |rects| are sorted so we just need to scan from left to right and compare + // it to the tab favicon positions from left to right. + int t = 0; + for (int r = 0; r < num_rects; ++r) { + while (t < GetTabCount()) { + TabGtk* tab = GetTabAt(t); + if (GdkRectMatchesTabFavIconBounds(rects[r], tab)) { + tabs_to_paint->push_back(t); + ++t; + break; + } + ++t; + } + } + return static_cast<int>(tabs_to_paint->size()) == num_rects; +} + +void TabStripGtk::PaintOnlyFavIcons(GdkEventExpose* event, + const std::vector<int>& tabs_to_paint) { + for (size_t i = 0; i < tabs_to_paint.size(); ++i) + GetTabAt(tabs_to_paint[i])->PaintFavIconArea(event); +} + +CustomDrawButton* TabStripGtk::MakeNewTabButton() { + CustomDrawButton* button = new CustomDrawButton(IDR_NEWTAB_BUTTON, + IDR_NEWTAB_BUTTON_P, IDR_NEWTAB_BUTTON_H, 0); + + // Let the middle mouse button initiate clicks as well. + gtk_util::SetButtonTriggersNavigation(button->widget()); + g_signal_connect(button->widget(), "clicked", + G_CALLBACK(OnNewTabClickedThunk), this); + GTK_WIDGET_UNSET_FLAGS(button->widget(), GTK_CAN_FOCUS); + gtk_fixed_put(GTK_FIXED(tabstrip_.get()), button->widget(), 0, 0); + + return button; +} |