// Copyright (c) 2009 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 "app/gfx/chrome_canvas.h" #include "app/l10n_util.h" #include "app/resource_bundle.h" #include "app/slide_animation.h" #include "base/gfx/gtk_util.h" #include "base/gfx/point.h" #include "chrome/browser/browser.h" #include "chrome/browser/gtk/custom_button.h" #include "chrome/browser/gtk/tabs/dragged_tab_controller_gtk.h" #include "chrome/browser/tab_contents/tab_contents.h" #include "grit/generated_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 kNewTabButtonHOffset = -5; const int kNewTabButtonVOffset = 5; const int kHorizontalMoveThreshold = 16; // pixels // The horizontal offset from one tab to the next, // which results in overlapping tabs. const int kTabHOffset = -16; SkBitmap* background = NULL; inline int Round(double x) { return static_cast(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); } } // 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, SNAP }; 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(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(TabStripGtk* tabstrip, TabStripGtk::TabAnimation* animation, int index) { double unselected, selected; tabstrip->GetCurrentTabWidths(&unselected, &selected); TabGtk* 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_); // TODO(jhawkins): Remove this once each tab is its own widget. SimulateMouseMotion(); // 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(TabRendererGtk::GetStandardSize().width()); if ((end_tab_count - start_tab_count) > 0 && start_unselected_width_ < standard_tab_width) { double minimum_tab_width = static_cast( TabRendererGtk::GetMinimumUnselectedSize().width()); start_unselected_width_ -= minimum_tab_width / start_tab_count; } tabstrip_->GenerateIdealBounds(); tabstrip_->GetDesiredTabWidths(end_tab_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: // When the animation completes, we send the Container 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, or // resets the hover index if it's now stale. void SimulateMouseMotion() { // Get default display and screen. GdkDisplay* display = gdk_display_get_default(); GdkScreen* screen = gdk_display_get_default_screen(display); // Get cursor position. int x, y; gdk_display_get_pointer(display, NULL, &x, &y, NULL); // Reset cursor position. gdk_display_warp_pointer(display, screen, x, y); } // 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(); GenerateStartAndEndWidths(tab_count - 1, tab_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 target_width = is_selected ? end_unselected_width_ : end_selected_width_; double 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)->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(); GenerateStartAndEndWidths(tab_count, tab_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. // 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); double delta = start_width - target_width; return start_width - (delta * animation_.GetCurrentValue()); } 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), tab_a_->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), tab_b_->y(), tab_b_->width(), tab_b_->height()); tabstrip_->SetTabBounds(tab_b_, bounds); } protected: // Overridden from TabStrip::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(); GenerateStartAndEndWidths(tab_count, tab_count); InitStartState(); } virtual ~ResizeLayoutAnimation() {} // Overridden from AnimationDelegate: virtual void AnimationEnded(const Animation* animation) { tabstrip_->resize_layout_scheduled_ = false; TabStripGtk::TabAnimation::AnimationEnded(animation); } protected: // Overridden from TabStripGtk::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) { TabGtk* current_tab = tabstrip_->GetTabAt(i); if (current_tab->IsSelected()) { start_selected_width_ = current_tab->width(); } else { start_unselected_width_ = current_tab->width(); } } } DISALLOW_COPY_AND_ASSIGN(ResizeLayoutAnimation); }; //////////////////////////////////////////////////////////////////////////////// // TabStripGtk, public: TabStripGtk::TabStripGtk(TabStripModel* model) : current_unselected_width_(TabGtk::GetStandardSize().width()), current_selected_width_(TabGtk::GetStandardSize().width()), available_width_for_tabs_(-1), resize_layout_scheduled_(false), model_(model) { } 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::iterator iterator = tab_data_.begin(); for (; iterator < tab_data_.end(); iterator++) { delete iterator->tab; } tab_data_.clear(); } void TabStripGtk::Init(int width) { ResourceBundle &rb = ResourceBundle::GetSharedInstance(); model_->AddObserver(this); if (!background) { background = rb.GetBitmapNamed(IDR_WINDOW_TOP_CENTER); } tabstrip_.Own(gtk_fixed_new()); gtk_fixed_set_has_window(GTK_FIXED(tabstrip_.get()), TRUE); gtk_widget_set_size_request(tabstrip_.get(), width, TabGtk::GetMinimumUnselectedSize().height()); gtk_widget_set_app_paintable(tabstrip_.get(), TRUE); g_signal_connect(G_OBJECT(tabstrip_.get()), "expose-event", G_CALLBACK(OnExpose), this); g_signal_connect(G_OBJECT(tabstrip_.get()), "size-allocate", G_CALLBACK(OnSizeAllocate), this); newtab_button_.reset(MakeNewTabButton()); gtk_widget_show_all(tabstrip_.get()); bounds_ = GetInitialWidgetBounds(tabstrip_.get()); } void TabStripGtk::AddTabStripToBox(GtkWidget* box) { gtk_box_pack_start(GTK_BOX(box), tabstrip_.get(), FALSE, FALSE, 0); } void TabStripGtk::Show() { gtk_widget_show(tabstrip_.get()); } void TabStripGtk::Hide() { gtk_widget_hide(tabstrip_.get()); } void TabStripGtk::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; TabGtk* tab = GetTabAt(i); SetTabBounds(tab, bounds); tab_right = bounds.right() + kTabHOffset; } LayoutNewTabButton(static_cast(tab_right), current_unselected_width_); 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 { TabContents* contents = model_->GetTabContentsAt(index); if (!contents || !contents->is_loading()) { current_tab->ValidateLoadingAnimation(TabGtk::ANIMATION_NONE); } else if (contents->waiting_for_response()) { current_tab->ValidateLoadingAnimation(TabGtk::ANIMATION_WAITING); } else { current_tab->ValidateLoadingAnimation(TabGtk::ANIMATION_LOADING); } } } gtk_widget_queue_draw(tabstrip_.get()); } bool TabStripGtk::IsAnimating() const { return active_animation_.get() != NULL; } void TabStripGtk::DestroyDragController() { if (IsDragSessionActive()) drag_controller_.reset(NULL); } gfx::Rect TabStripGtk::GetIdealBounds(int index) { DCHECK(index >= 0 && index < GetTabCount()); return tab_data_.at(index).ideal_bounds; } //////////////////////////////////////////////////////////////////////////////// // TabStripGtk, TabStripModelObserver implementation: void TabStripGtk::TabInsertedAt(TabContents* contents, int index, bool foreground) { DCHECK(contents); DCHECK(index == TabStripModel::kNoTab || model_->ContainsIndex(index)); if (active_animation_.get()) active_animation_->Stop(); TabGtk* tab = new TabGtk(this); // Only insert if we're not already in the list. if (index == TabStripModel::kNoTab) { TabData d = { tab, gfx::Rect() }; tab_data_.push_back(d); tab->UpdateData(contents, false); } else { TabData d = { tab, gfx::Rect() }; tab_data_.insert(tab_data_.begin() + index, d); tab->UpdateData(contents, false); } 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); } else { Layout(); } } void TabStripGtk::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 TabStripGtk::TabSelectedAt(TabContents* old_contents, TabContents* new_contents, int index, bool user_gesture) { DCHECK(index >= 0 && index < static_cast(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 { gtk_widget_queue_draw(tabstrip_.get()); } } } void TabStripGtk::TabMoved(TabContents* contents, int from_index, int to_index) { TabGtk* tab = GetTabAt(from_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 TabStripGtk::TabChangedAt(TabContents* contents, int index, bool loading_only) { // Index is in terms of the model. Need to make sure we adjust that index in // case we have an animation going. TabGtk* tab = GetTabAt(index); tab->UpdateData(contents, loading_only); tab->UpdateFromModel(); gtk_widget_queue_draw(tabstrip_.get()); } //////////////////////////////////////////////////////////////////////////////// // TabStripGtk, TabGtk::TabDelegate implementation: bool TabStripGtk::IsTabSelected(const TabGtk* tab) const { if (tab->closing()) return false; return GetIndexOfTab(tab) == model_->selected_index(); } void TabStripGtk::GetCurrentTabWidths(double* unselected_width, double* selected_width) const { *unselected_width = current_unselected_width_; *selected_width = current_selected_width_; } 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); resize_layout_scheduled_ = true; model_->CloseTabContentsAt(tab_index); } } 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::CommandCloseTabsOpenedBy) { int index = GetIndexOfTab(tab); if (model_->ContainsIndex(index)) { std::vector indices = model_->GetIndexesOpenedBy(index); std::vector::const_iterator iter = indices.begin(); for (; iter != indices.end(); ++iter) { int current_index = *iter; DCHECK(current_index >= 0 && current_index < GetTabCount()); } } } } void TabStripGtk::StopHighlightTabsForCommand( TabStripModel::ContextMenuCommand command_id, TabGtk* 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 TabStripGtk::StopAllHighlighting() { // TODO(jhawkins): Hook up animations. } 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; } //////////////////////////////////////////////////////////////////////////////// // TabStripGtk, private: int TabStripGtk::GetTabCount() const { return static_cast(tab_data_.size()); } int TabStripGtk::GetAvailableWidthForTabs(TabGtk* last_tab) const { return last_tab->x() + last_tab->width(); } 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; } 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); delete removed; } void TabStripGtk::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 = TabGtk::GetStandardSize().height(); double tab_x = 0; for (int i = 0; i < tab_count; ++i) { TabGtk* 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 TabStripGtk::LayoutNewTabButton(double last_tab_right, double unselected_width) { int delta = abs(Round(unselected_width) - TabGtk::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. gtk_fixed_move(GTK_FIXED(tabstrip_.get()), newtab_button_.get()->widget(), bounds_.width() - newtab_button_.get()->width(), kNewTabButtonVOffset); } else { gtk_fixed_move(GTK_FIXED(tabstrip_.get()), newtab_button_.get()->widget(), Round(last_tab_right - kTabHOffset) + kNewTabButtonHOffset, kNewTabButtonVOffset); } } void TabStripGtk::GetDesiredTabWidths(int tab_count, double* unselected_width, double* selected_width) const { const double min_unselected_width = TabGtk::GetMinimumUnselectedSize().width(); const double min_selected_width = TabGtk::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 = tabstrip_.get()->allocation.width; if (available_width_for_tabs_ < 0) { available_width = bounds_.width(); available_width -= (kNewTabButtonHOffset + newtab_button_.get()->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(available_width - total_offset) / static_cast(tab_count)), static_cast(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( available_width - total_offset - min_selected_width) / static_cast(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); } } } void TabStripGtk::ResizeLayoutTabs() { available_width_for_tabs_ = -1; double unselected, selected; GetDesiredTabWidths(GetTabCount(), &unselected, &selected); TabGtk* 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->width() - w) > 1) StartResizeLayoutAnimation(); } // Called from: // - animation tick void TabStripGtk::AnimationLayout(double unselected_width) { int tab_height = TabGtk::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); 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 + kTabHOffset; } LayoutNewTabButton(tab_x, unselected_width); gtk_widget_queue_draw(tabstrip_.get()); } void TabStripGtk::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 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) { if (active_animation_.get()) active_animation_->Stop(); active_animation_.reset(new MoveTabAnimation(this, from_index, to_index)); active_animation_->Start(); } void TabStripGtk::StartResizeLayoutAnimation() { if (active_animation_.get()) active_animation_->Stop(); active_animation_.reset(new ResizeLayoutAnimation(this)); active_animation_->Start(); } bool TabStripGtk::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 TabStripGtk::FinishAnimation(TabStripGtk::TabAnimation* animation, bool layout) { active_animation_.reset(NULL); if (layout) Layout(); } // static gboolean TabStripGtk::OnExpose(GtkWidget* widget, GdkEventExpose* event, TabStripGtk* tabstrip) { if (gdk_region_empty(event->region)) return TRUE; // 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 = tabstrip->bounds_.width(); event->area.height = tabstrip->bounds_.height(); gdk_region_union_with_rect(event->region, &event->area); tabstrip->PaintBackground(event); // Paint the New Tab button. gtk_container_propagate_expose(GTK_CONTAINER(tabstrip->tabstrip_.get()), tabstrip->newtab_button_.get()->widget(), event); // Paint the tabs in reverse order, so they stack to the left. TabGtk* selected_tab = NULL; int tab_count = tabstrip->GetTabCount(); for (int i = tab_count - 1; i >= 0; --i) { TabGtk* tab = tabstrip->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->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->tabstrip_.get()), selected_tab->widget(), event); } return TRUE; } // static void TabStripGtk::OnSizeAllocate(GtkWidget* widget, GtkAllocation* allocation, TabStripGtk* tabstrip) { 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 (tabstrip->bounds_ == bounds) return; tabstrip->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 (tabstrip->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 (tabstrip->GetTabCount() == 1) tabstrip->Layout(); else tabstrip->ResizeLayoutTabs(); } // static void TabStripGtk::OnNewTabClicked(GtkWidget* widget, TabStripGtk* tabstrip) { tabstrip->model_->delegate()->AddBlankTab(true); } void TabStripGtk::PaintBackground(GdkEventExpose* event) { ChromeCanvasPaint canvas(event); canvas.TileImageInt(*background, 0, 0, bounds_.width(), bounds_.height()); } void TabStripGtk::SetTabBounds(TabGtk* tab, const gfx::Rect& bounds) { tab->SetBounds(bounds); gtk_fixed_move(GTK_FIXED(tabstrip_.get()), tab->widget(), bounds.x(), bounds.y()); } CustomDrawButton* TabStripGtk::MakeNewTabButton() { CustomDrawButton* button = new CustomDrawButton(IDR_NEWTAB_BUTTON, IDR_NEWTAB_BUTTON_P, IDR_NEWTAB_BUTTON_H, 0); g_signal_connect(G_OBJECT(button->widget()), "clicked", G_CALLBACK(OnNewTabClicked), this); GTK_WIDGET_UNSET_FLAGS(button->widget(), GTK_CAN_FOCUS); gtk_fixed_put(GTK_FIXED(tabstrip_.get()), button->widget(), 0, 0); return button; }