diff options
author | miu@chromium.org <miu@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-10-16 02:10:06 +0000 |
---|---|---|
committer | miu@chromium.org <miu@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-10-16 02:10:06 +0000 |
commit | 84f215956d399005923cc407b16655b16b95c2b7 (patch) | |
tree | 22beb9261311cd76ab04620b48660ed239b9ae73 | |
parent | 0686352174278600afdcf80398848faa0b6e266b (diff) | |
download | chromium_src-84f215956d399005923cc407b16655b16b95c2b7.zip chromium_src-84f215956d399005923cc407b16655b16b95c2b7.tar.gz chromium_src-84f215956d399005923cc407b16655b16b95c2b7.tar.bz2 |
New tab UI media indicator for recording, tab capture, and audio playback.
Implementation of new tab media indicator designed by the UX team. This replaces the existing "throbber animation over favicon" indicators with a simpler, less-distracting static icon (right-aligned).
1. Pulled out all existing recording/capture layout, animation, paint code, and graphics.
2. Modified existing audio playback indicator to become a multi-purpose media indicator.
3. Updated visibility precedence logic for media indicator, per discussion in bug.
4. Refactored duplicated code into tab_utils.h/.cc.
Testing: Updated unit tests. Manual look-and-feel testing to confirm correct behavior and animation transition smoothness on all of: Aura, Win non-Aura, Mac, and GTK.
BUG=290550
Review URL: https://codereview.chromium.org/26922003
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@228838 0039d316-1c4b-4281-b951-d872f2087c98
39 files changed, 827 insertions, 1232 deletions
diff --git a/chrome/app/theme/default_100_percent/common/tab_capture_indicator.png b/chrome/app/theme/default_100_percent/common/tab_capture_indicator.png Binary files differnew file mode 100644 index 0000000..38d1b44 --- /dev/null +++ b/chrome/app/theme/default_100_percent/common/tab_capture_indicator.png diff --git a/chrome/app/theme/default_100_percent/common/tab_recording_indicator.png b/chrome/app/theme/default_100_percent/common/tab_recording_indicator.png Binary files differnew file mode 100644 index 0000000..b4b6e54 --- /dev/null +++ b/chrome/app/theme/default_100_percent/common/tab_recording_indicator.png diff --git a/chrome/app/theme/default_100_percent/common/tab_recording_mask.png b/chrome/app/theme/default_100_percent/common/tab_recording_mask.png Binary files differdeleted file mode 100644 index 2964f09..0000000 --- a/chrome/app/theme/default_100_percent/common/tab_recording_mask.png +++ /dev/null diff --git a/chrome/app/theme/default_100_percent/tab_capture.png b/chrome/app/theme/default_100_percent/tab_capture.png Binary files differdeleted file mode 100644 index db742ed..0000000 --- a/chrome/app/theme/default_100_percent/tab_capture.png +++ /dev/null diff --git a/chrome/app/theme/default_100_percent/tab_capture_glow.png b/chrome/app/theme/default_100_percent/tab_capture_glow.png Binary files differdeleted file mode 100644 index 8054e5f..0000000 --- a/chrome/app/theme/default_100_percent/tab_capture_glow.png +++ /dev/null diff --git a/chrome/app/theme/default_100_percent/tab_recording.png b/chrome/app/theme/default_100_percent/tab_recording.png Binary files differdeleted file mode 100644 index a722411..0000000 --- a/chrome/app/theme/default_100_percent/tab_recording.png +++ /dev/null diff --git a/chrome/app/theme/default_200_percent/common/tab_capture_indicator.png b/chrome/app/theme/default_200_percent/common/tab_capture_indicator.png Binary files differnew file mode 100644 index 0000000..06ae81e --- /dev/null +++ b/chrome/app/theme/default_200_percent/common/tab_capture_indicator.png diff --git a/chrome/app/theme/default_200_percent/common/tab_recording_indicator.png b/chrome/app/theme/default_200_percent/common/tab_recording_indicator.png Binary files differnew file mode 100644 index 0000000..6ed9203 --- /dev/null +++ b/chrome/app/theme/default_200_percent/common/tab_recording_indicator.png diff --git a/chrome/app/theme/default_200_percent/common/tab_recording_mask.png b/chrome/app/theme/default_200_percent/common/tab_recording_mask.png Binary files differdeleted file mode 100644 index c04537d..0000000 --- a/chrome/app/theme/default_200_percent/common/tab_recording_mask.png +++ /dev/null diff --git a/chrome/app/theme/default_200_percent/tab_capture.png b/chrome/app/theme/default_200_percent/tab_capture.png Binary files differdeleted file mode 100644 index e7f397b..0000000 --- a/chrome/app/theme/default_200_percent/tab_capture.png +++ /dev/null diff --git a/chrome/app/theme/default_200_percent/tab_capture_glow.png b/chrome/app/theme/default_200_percent/tab_capture_glow.png Binary files differdeleted file mode 100644 index 32f481d..0000000 --- a/chrome/app/theme/default_200_percent/tab_capture_glow.png +++ /dev/null diff --git a/chrome/app/theme/default_200_percent/tab_recording.png b/chrome/app/theme/default_200_percent/tab_recording.png Binary files differdeleted file mode 100644 index 13cdf2f..0000000 --- a/chrome/app/theme/default_200_percent/tab_recording.png +++ /dev/null diff --git a/chrome/app/theme/theme_resources.grd b/chrome/app/theme/theme_resources.grd index 041907b..d50f1c5 100644 --- a/chrome/app/theme/theme_resources.grd +++ b/chrome/app/theme/theme_resources.grd @@ -813,6 +813,7 @@ <structure type="chrome_scaled_image" name="IDR_CLOSE_1_MASK" file="common/close_1_mask.png" /> <structure type="chrome_scaled_image" name="IDR_CLOSE_1_P" file="common/close_1_pressed.png" /> <structure type="chrome_scaled_image" name="IDR_TAB_AUDIO_INDICATOR" file="common/tab_audio_indicator.png" /> + <structure type="chrome_scaled_image" name="IDR_TAB_CAPTURE_INDICATOR" file="common/tab_capture_indicator.png" /> <structure type="chrome_scaled_image" name="IDR_TAB_DROP_DOWN" file="tab_drop_down.png" /> <structure type="chrome_scaled_image" name="IDR_TAB_DROP_UP" file="tab_drop_up.png" /> <if expr="not pp_ifdef('toolkit_views') and not is_macosx and not is_ios"> @@ -830,6 +831,7 @@ <structure type="chrome_scaled_image" name="IDR_TAB_INACTIVE_LEFT" file="mac/tab_inactive_left.png" /> <structure type="chrome_scaled_image" name="IDR_TAB_INACTIVE_RIGHT" file="mac/tab_inactive_right.png" /> </if> + <structure type="chrome_scaled_image" name="IDR_TAB_RECORDING_INDICATOR" file="common/tab_recording_indicator.png" /> <structure type="chrome_scaled_image" name="IDR_TABLET_FAVICON" file="common/favicon_tablet.png" /> <if expr="pp_ifdef('chromeos')"> <structure type="chrome_scaled_image" name="IDR_TECHNICAL_ERROR" file="cros/technical_error.png" /> @@ -897,10 +899,6 @@ <structure type="chrome_scaled_image" name="IDR_THROBBER_LIGHT" file="throbber_light.png" /> <structure type="chrome_scaled_image" name="IDR_THROBBER_WAITING" file="throbber_waiting.png" /> <structure type="chrome_scaled_image" name="IDR_THROBBER_WAITING_LIGHT" file="throbber_waiting_light.png" /> - <structure type="chrome_scaled_image" name="IDR_TAB_RECORDING" file="tab_recording.png" /> - <structure type="chrome_scaled_image" name="IDR_TAB_RECORDING_MASK" file="common/tab_recording_mask.png" /> - <structure type="chrome_scaled_image" name="IDR_TAB_CAPTURE" file="tab_capture.png" /> - <structure type="chrome_scaled_image" name="IDR_TAB_CAPTURE_GLOW" file="tab_capture_glow.png" /> <if expr="pp_ifdef('use_ash')"> <structure type="chrome_scaled_image" name="IDR_TOOLBAR_SHADE_BOTTOM" file="common/toolbar_shade_bottom.png" /> <structure type="chrome_scaled_image" name="IDR_TOOLBAR_SHADE_LEFT" file="common/toolbar_shade_left.png" /> diff --git a/chrome/browser/ui/cocoa/tabs/media_indicator_view.h b/chrome/browser/ui/cocoa/tabs/media_indicator_view.h new file mode 100644 index 0000000..176e56e --- /dev/null +++ b/chrome/browser/ui/cocoa/tabs/media_indicator_view.h @@ -0,0 +1,44 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_UI_COCOA_TABS_MEDIA_INDICATOR_VIEW_H_ +#define CHROME_BROWSER_UI_COCOA_TABS_MEDIA_INDICATOR_VIEW_H_ + +#import <Cocoa/Cocoa.h> + +#include "base/memory/scoped_ptr.h" +#include "chrome/browser/ui/tabs/tab_utils.h" + +class MediaIndicatorViewAnimationDelegate; + +namespace gfx { +class Animation; +} // namespace gfx + +@interface MediaIndicatorView : NSImageView { + @private + TabMediaState mediaState_; + scoped_ptr<MediaIndicatorViewAnimationDelegate> delegate_; + scoped_ptr<gfx::Animation> animation_; + TabMediaState animatingMediaState_; +} + +@property(readonly, nonatomic) TabMediaState mediaState; +@property(readonly, nonatomic) TabMediaState animatingMediaState; + +// Initialize a new MediaIndicatorView in TAB_MEDIA_STATE_NONE (i.e., a +// non-active indicator). +- (id)init; + +// Starts the animation to transition the indicator to the new |mediaState|. +- (void)updateIndicator:(TabMediaState)mediaState; + +@end + +@interface MediaIndicatorView(TestingAPI) +// Turns off animations for logic testing. +- (void)disableAnimations; +@end + +#endif // CHROME_BROWSER_UI_COCOA_TABS_MEDIA_INDICATOR_VIEW_H_ diff --git a/chrome/browser/ui/cocoa/tabs/media_indicator_view.mm b/chrome/browser/ui/cocoa/tabs/media_indicator_view.mm new file mode 100644 index 0000000..e3caf07 --- /dev/null +++ b/chrome/browser/ui/cocoa/tabs/media_indicator_view.mm @@ -0,0 +1,100 @@ +// Copyright 2013 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. + +#import "chrome/browser/ui/cocoa/tabs/media_indicator_view.h" + +#include "ui/gfx/animation/animation.h" +#include "ui/gfx/animation/animation_delegate.h" +#include "ui/gfx/image/image.h" + +class MediaIndicatorViewAnimationDelegate : public gfx::AnimationDelegate { + public: + MediaIndicatorViewAnimationDelegate(NSView* view, + TabMediaState* mediaState, + TabMediaState* animatingMediaState) + : view_(view), mediaState_(mediaState), + animatingMediaState_(animatingMediaState) {} + virtual ~MediaIndicatorViewAnimationDelegate() {} + + virtual void AnimationEnded(const gfx::Animation* animation) OVERRIDE { + *animatingMediaState_ = *mediaState_; + [view_ setNeedsDisplay:YES]; + } + virtual void AnimationProgressed(const gfx::Animation* animation) OVERRIDE { + [view_ setNeedsDisplay:YES]; + } + virtual void AnimationCanceled(const gfx::Animation* animation) OVERRIDE { + *animatingMediaState_ = *mediaState_; + [view_ setNeedsDisplay:YES]; + } + + private: + NSView* const view_; + TabMediaState* const mediaState_; + TabMediaState* const animatingMediaState_; +}; + +@implementation MediaIndicatorView + +@synthesize mediaState = mediaState_; +@synthesize animatingMediaState = animatingMediaState_; + +- (id)init { + if ((self = [super initWithFrame:NSZeroRect])) { + mediaState_ = animatingMediaState_ = TAB_MEDIA_STATE_NONE; + delegate_.reset(new MediaIndicatorViewAnimationDelegate( + self, &mediaState_, &animatingMediaState_)); + } + return self; +} + +- (void)updateIndicator:(TabMediaState)mediaState { + if (mediaState == mediaState_) + return; + + mediaState_ = mediaState; + animation_.reset(); + + // Prepare this view if the new TabMediaState is an active one. + if (mediaState_ != TAB_MEDIA_STATE_NONE) { + animatingMediaState_ = mediaState_; + NSImage* const image = + chrome::GetTabMediaIndicatorImage(mediaState_).ToNSImage(); + NSRect frame = [self frame]; + frame.size = [image size]; + [self setFrame:frame]; + [self setImage:image]; + } + + // If the animation delegate is missing, that means animations were disabled + // for testing; so, go directly to animating completion state. + if (!delegate_) { + animatingMediaState_ = mediaState_; + return; + } + + animation_ = chrome::CreateTabMediaIndicatorFadeAnimation(mediaState_); + animation_->set_delegate(delegate_.get()); + animation_->Start(); +} + +- (void)drawRect:(NSRect)rect { + if (!animation_) + return; + + double opaqueness = animation_->GetCurrentValue(); + if (mediaState_ == TAB_MEDIA_STATE_NONE) + opaqueness = 1.0 - opaqueness; // Fading out, not in. + + [[self image] drawInRect:[self bounds] + fromRect:NSZeroRect + operation:NSCompositeSourceOver + fraction:opaqueness]; +} + +- (void)disableAnimations { + delegate_.reset(); +} + +@end diff --git a/chrome/browser/ui/cocoa/tabs/media_indicator_view_unittest.mm b/chrome/browser/ui/cocoa/tabs/media_indicator_view_unittest.mm new file mode 100644 index 0000000..f3eb132 --- /dev/null +++ b/chrome/browser/ui/cocoa/tabs/media_indicator_view_unittest.mm @@ -0,0 +1,34 @@ +// Copyright 2013 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. + +#import "chrome/browser/ui/cocoa/tabs/media_indicator_view.h" + +#include "base/mac/scoped_nsobject.h" +#include "base/message_loop/message_loop.h" +#import "chrome/browser/ui/cocoa/cocoa_test_helper.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/platform_test.h" + +namespace { + +class MediaIndicatorViewTest : public CocoaTest { + public: + MediaIndicatorViewTest() { + view_.reset([[MediaIndicatorView alloc] init]); + ASSERT_TRUE(!!view_); + EXPECT_EQ(TAB_MEDIA_STATE_NONE, [view_ animatingMediaState]); + + [[test_window() contentView] addSubview:view_.get()]; + + [view_ updateIndicator:TAB_MEDIA_STATE_AUDIO_PLAYING]; + EXPECT_EQ(TAB_MEDIA_STATE_AUDIO_PLAYING, [view_ animatingMediaState]); + } + + base::scoped_nsobject<MediaIndicatorView> view_; + base::MessageLoopForUI message_loop_; // Needed for gfx::Animation. +}; + +TEST_VIEW(MediaIndicatorViewTest, view_) + +} // namespace diff --git a/chrome/browser/ui/cocoa/tabs/tab_controller.h b/chrome/browser/ui/cocoa/tabs/tab_controller.h index 2cfef89..ad0a6a1 100644 --- a/chrome/browser/ui/cocoa/tabs/tab_controller.h +++ b/chrome/browser/ui/cocoa/tabs/tab_controller.h @@ -20,6 +20,7 @@ enum TabLoadingState { kTabCrashed, }; +@class MediaIndicatorView; @class MenuController; namespace TabControllerInternal { class MenuDelegate; @@ -42,7 +43,7 @@ class MenuDelegate; @private base::scoped_nsobject<NSView> iconView_; base::scoped_nsobject<NSTextField> titleView_; - base::scoped_nsobject<NSView> audioIndicatorView_; + base::scoped_nsobject<MediaIndicatorView> mediaIndicatorView_; base::scoped_nsobject<HoverCloseButton> closeButton_; NSRect originalIconFrame_; // frame of iconView_ as loaded from nib @@ -51,12 +52,10 @@ class MenuDelegate; BOOL app_; BOOL mini_; BOOL pinned_; - BOOL projecting_; BOOL active_; BOOL selected_; GURL url_; TabLoadingState loadingState_; - CGFloat iconTitleXOffset_; // between left edges of icon and title id<TabControllerTarget> target_; // weak, where actions are sent SEL action_; // selector sent when tab is selected by clicking scoped_ptr<ui::SimpleMenuModel> contextMenuModel_; @@ -70,10 +69,6 @@ class MenuDelegate; @property(assign, nonatomic) BOOL app; @property(assign, nonatomic) BOOL mini; @property(assign, nonatomic) BOOL pinned; -// A tab is called "projecting" when a video/audio stream of its contents is -// being captured and perhaps streamed remotely. We add a favicon glow animation -// in this state to notify the user. -@property(assign, nonatomic) BOOL projecting; // Note that |-selected| will return YES if the controller is |-active|, too. // |-setSelected:| affects the selection, while |-setActive:| affects the key // status/focus of the content. @@ -83,7 +78,7 @@ class MenuDelegate; @property(assign, nonatomic) GURL url; @property(assign, nonatomic) NSView* iconView; @property(readonly, nonatomic) NSTextField* titleView; -@property(assign, nonatomic) NSView* audioIndicatorView; +@property(assign, nonatomic) MediaIndicatorView* mediaIndicatorView; @property(readonly, nonatomic) HoverCloseButton* closeButton; // Minimum and maximum allowable tab width. The minimum width does not show @@ -123,7 +118,7 @@ class MenuDelegate; - (NSString*)toolTip; - (int)iconCapacity; - (BOOL)shouldShowIcon; -- (BOOL)shouldShowAudioIndicator; +- (BOOL)shouldShowMediaIndicator; - (BOOL)shouldShowCloseButton; @end // TabController(TestingAPI) diff --git a/chrome/browser/ui/cocoa/tabs/tab_controller.mm b/chrome/browser/ui/cocoa/tabs/tab_controller.mm index 8185c7f..ff67592 100644 --- a/chrome/browser/ui/cocoa/tabs/tab_controller.mm +++ b/chrome/browser/ui/cocoa/tabs/tab_controller.mm @@ -4,12 +4,14 @@ #import "chrome/browser/ui/cocoa/tabs/tab_controller.h" +#include <algorithm> #include <cmath> #include "base/mac/bundle_locations.h" #include "base/mac/mac_util.h" #import "chrome/browser/themes/theme_properties.h" #import "chrome/browser/themes/theme_service.h" +#import "chrome/browser/ui/cocoa/tabs/media_indicator_view.h" #import "chrome/browser/ui/cocoa/tabs/tab_controller_target.h" #import "chrome/browser/ui/cocoa/tabs/tab_view.h" #import "chrome/browser/ui/cocoa/themed_window.h" @@ -26,7 +28,6 @@ @synthesize loadingState = loadingState_; @synthesize mini = mini_; @synthesize pinned = pinned_; -@synthesize projecting = projecting_; @synthesize target = target_; @synthesize url = url_; @@ -97,7 +98,6 @@ class MenuDelegate : public ui::SimpleMenuModel::Delegate { // explicilty save the offset between the title and the close button since // we can just get that value for the close button's frame. NSRect titleFrame = NSMakeRect(35, 6, 92, 14); - iconTitleXOffset_ = NSMinX(titleFrame) - NSMinX(originalIconFrame_); // Label. titleView_.reset([[NSTextField alloc] initWithFrame:titleFrame]); @@ -230,24 +230,8 @@ class MenuDelegate : public ui::SimpleMenuModel::Delegate { - (void)setIconView:(NSView*)iconView { [iconView_ removeFromSuperview]; iconView_.reset([iconView retain]); - if ([self projecting] && [self loadingState] == kTabDone) { - // When projecting we have bigger iconView to accommodate the glow - // animation, so this frame should be double the size of a favicon. - NSRect iconFrame = [iconView frame]; - // Center the iconView given it's double regular size. - if ([self app] || [self mini]) { - const CGFloat tabWidth = [self app] ? [TabController appTabWidth] - : [TabController miniTabWidth]; - iconFrame.origin.x = std::floor((tabWidth - NSWidth(iconFrame)) / 2.0); - } else { - iconFrame.origin.x = std::floor(originalIconFrame_.origin.x / 2.0); - } - - iconFrame.origin.y = -std::ceil(originalIconFrame_.origin.y / 2.0); - - [iconView_ setFrame:iconFrame]; - } else if ([self app] || [self mini]) { + if ([self app] || [self mini]) { NSRect appIconFrame = [iconView frame]; appIconFrame.origin = originalIconFrame_.origin; @@ -273,18 +257,16 @@ class MenuDelegate : public ui::SimpleMenuModel::Delegate { return titleView_; } -- (NSView*)audioIndicatorView { - return audioIndicatorView_; +- (MediaIndicatorView*)mediaIndicatorView { + return mediaIndicatorView_; } -- (void)setAudioIndicatorView:(NSView*)audioIndicatorView { - if (audioIndicatorView == audioIndicatorView_) - return; - [audioIndicatorView_ removeFromSuperview]; - audioIndicatorView_.reset([audioIndicatorView retain]); +- (void)setMediaIndicatorView:(MediaIndicatorView*)mediaIndicatorView { + [mediaIndicatorView_ removeFromSuperview]; + mediaIndicatorView_.reset([mediaIndicatorView retain]); [self updateVisibility]; - if (audioIndicatorView_) - [[self view] addSubview:audioIndicatorView_]; + if (mediaIndicatorView_) + [[self view] addSubview:mediaIndicatorView_]; } - (HoverCloseButton*)closeButton { @@ -299,58 +281,35 @@ class MenuDelegate : public ui::SimpleMenuModel::Delegate { // tab. We never actually do this, but it's a helpful guide for determining // how much space we have available. - (int)iconCapacity { - CGFloat width = NSMaxX([closeButton_ frame]) - NSMinX(originalIconFrame_); + const CGFloat availableWidth = std::max<CGFloat>( + 0, NSMaxX([closeButton_ frame]) - NSMinX(originalIconFrame_)); + const CGFloat widthPerIcon = NSWidth(originalIconFrame_); const int kPaddingBetweenIcons = 2; - CGFloat iconWidth = NSWidth(originalIconFrame_) + kPaddingBetweenIcons; - - return width / iconWidth; + if (availableWidth >= widthPerIcon && + availableWidth < (widthPerIcon + kPaddingBetweenIcons)) { + return 1; + } + return availableWidth / (widthPerIcon + kPaddingBetweenIcons); } -// Returns YES if we should show the icon. When tabs get too small, we clip -// the favicon before the close button for selected tabs, and prefer the -// favicon for unselected tabs. Exception: We clip the favicon before the audio -// indicator in all cases. The icon can also be suppressed more directly -// by clearing iconView_. - (BOOL)shouldShowIcon { - if (!iconView_) - return NO; - const BOOL shouldShowAudioIndicator = [self shouldShowAudioIndicator]; - if ([self mini]) - return !shouldShowAudioIndicator; - int required_capacity = shouldShowAudioIndicator ? 2 : 1; - if ([self selected]) { - // Active tabs give priority to the close button, then the audio indicator, - // then the favicon. - ++required_capacity; - } else { - // Non-selected tabs give priority to the audio indicator, then the favicon, - // and finally the close button. - } - return [self iconCapacity] >= required_capacity; + return chrome::ShouldTabShowFavicon( + [self iconCapacity], [self mini], [self selected], iconView_ != nil, + !mediaIndicatorView_ ? TAB_MEDIA_STATE_NONE : + [mediaIndicatorView_ animatingMediaState]); } -// Returns YES if we should show the audio indicator. When tabs get too small, -// we clip the audio indicator before the close button for selected tabs, and -// prefer the audio indicator for unselected tabs. -- (BOOL)shouldShowAudioIndicator { - if (!audioIndicatorView_) +- (BOOL)shouldShowMediaIndicator { + if (!mediaIndicatorView_) return NO; - if ([self mini]) - return YES; - if ([self selected]) { - // The active tab clips the audio indicator before the close button. - return [self iconCapacity] >= 2; - } - // Non-selected tabs clip close button before the audio indicator. - return [self iconCapacity] >= 1; + return chrome::ShouldTabShowMediaIndicator( + [self iconCapacity], [self mini], [self selected], iconView_ != nil, + [mediaIndicatorView_ animatingMediaState]); } -// Returns YES if we should be showing the close button. The selected tab -// always shows the close button. - (BOOL)shouldShowCloseButton { - if ([self mini]) - return NO; - return ([self selected] || [self iconCapacity] >= 3); + return chrome::ShouldTabShowCloseButton( + [self iconCapacity], [self mini], [self selected]); } - (void)updateVisibility { @@ -369,32 +328,32 @@ class MenuDelegate : public ui::SimpleMenuModel::Delegate { [closeButton_ setHidden:!newShowCloseButton]; - BOOL newShowAudioIndicator = [self shouldShowAudioIndicator]; + BOOL newShowMediaIndicator = [self shouldShowMediaIndicator]; - if (audioIndicatorView_) { - [audioIndicatorView_ setHidden:!newShowAudioIndicator]; + [mediaIndicatorView_ setHidden:!newShowMediaIndicator]; - NSRect newFrame = [audioIndicatorView_ frame]; + if (newShowMediaIndicator) { + NSRect newFrame = [mediaIndicatorView_ frame]; if ([self app] || [self mini]) { - // Tab is pinned: Position the audio indicator in the center. + // Tab is pinned: Position the media indicator in the center. const CGFloat tabWidth = [self app] ? [TabController appTabWidth] : [TabController miniTabWidth]; newFrame.origin.x = std::floor((tabWidth - NSWidth(newFrame)) / 2); newFrame.origin.y = NSMinY(originalIconFrame_) - std::floor((NSHeight(newFrame) - NSHeight(originalIconFrame_)) / 2); } else { - // The Frame for the audioIndicatorView_ depends on whether iconView_ + // The Frame for the mediaIndicatorView_ depends on whether iconView_ // and/or closeButton_ are visible, and where they have been positioned. const NSRect closeButtonFrame = [closeButton_ frame]; newFrame.origin.x = NSMinX(closeButtonFrame); // Position to the left of the close button when it is showing. if (newShowCloseButton) newFrame.origin.x -= NSWidth(newFrame); - // Audio indicator is centered vertically, with respect to closeButton_. + // Media indicator is centered vertically, with respect to closeButton_. newFrame.origin.y = NSMinY(closeButtonFrame) - std::floor((NSHeight(newFrame) - NSHeight(closeButtonFrame)) / 2); } - [audioIndicatorView_ setFrame:newFrame]; + [mediaIndicatorView_ setFrame:newFrame]; } // Adjust the title view based on changes to the icon's and close button's @@ -405,13 +364,13 @@ class MenuDelegate : public ui::SimpleMenuModel::Delegate { newTitleFrame.origin.y = oldTitleFrame.origin.y; if (newShowIcon) { - newTitleFrame.origin.x = originalIconFrame_.origin.x + iconTitleXOffset_; + newTitleFrame.origin.x = NSMaxX([iconView_ frame]); } else { newTitleFrame.origin.x = originalIconFrame_.origin.x; } - if (newShowAudioIndicator) { - newTitleFrame.size.width = NSMinX([audioIndicatorView_ frame]) - + if (newShowMediaIndicator) { + newTitleFrame.size.width = NSMinX([mediaIndicatorView_ frame]) - newTitleFrame.origin.x; } else if (newShowCloseButton) { newTitleFrame.size.width = NSMinX([closeButton_ frame]) - diff --git a/chrome/browser/ui/cocoa/tabs/tab_controller_unittest.mm b/chrome/browser/ui/cocoa/tabs/tab_controller_unittest.mm index ab580340..b530b32 100644 --- a/chrome/browser/ui/cocoa/tabs/tab_controller_unittest.mm +++ b/chrome/browser/ui/cocoa/tabs/tab_controller_unittest.mm @@ -7,6 +7,7 @@ #import "base/mac/scoped_nsobject.h" #include "base/strings/utf_string_conversions.h" #include "chrome/browser/ui/cocoa/cocoa_test_helper.h" +#import "chrome/browser/ui/cocoa/tabs/media_indicator_view.h" #import "chrome/browser/ui/cocoa/tabs/tab_controller.h" #import "chrome/browser/ui/cocoa/tabs/tab_controller_target.h" #import "chrome/browser/ui/cocoa/tabs/tab_strip_drag_controller.h" @@ -117,12 +118,18 @@ class TabControllerTest : public CocoaTest { const TabController* controller) { // Check whether subviews should be visible when they are supposed to be, // given Tab size and TabRendererData state. + const TabMediaState indicatorState = + [[controller mediaIndicatorView] mediaState]; if ([controller mini]) { - if ([controller projecting]) + EXPECT_EQ(1, [controller iconCapacity]); + if (indicatorState == TAB_MEDIA_STATE_CAPTURING || + indicatorState == TAB_MEDIA_STATE_RECORDING) { + EXPECT_FALSE([controller shouldShowIcon]); + EXPECT_TRUE([controller shouldShowMediaIndicator]); + } else { EXPECT_TRUE([controller shouldShowIcon]); - else - EXPECT_TRUE([controller shouldShowIcon] != - [controller shouldShowAudioIndicator]); + EXPECT_FALSE([controller shouldShowMediaIndicator]); + } EXPECT_FALSE([controller shouldShowCloseButton]); } else if ([controller selected]) { EXPECT_TRUE([controller shouldShowCloseButton]); @@ -130,23 +137,25 @@ class TabControllerTest : public CocoaTest { case 0: case 1: EXPECT_FALSE([controller shouldShowIcon]); - EXPECT_FALSE([controller shouldShowAudioIndicator]); + EXPECT_FALSE([controller shouldShowMediaIndicator]); break; case 2: - if ([controller projecting]) + if (indicatorState == TAB_MEDIA_STATE_CAPTURING || + indicatorState == TAB_MEDIA_STATE_RECORDING) { + EXPECT_FALSE([controller shouldShowIcon]); + EXPECT_TRUE([controller shouldShowMediaIndicator]); + } else { EXPECT_TRUE([controller shouldShowIcon]); - else - EXPECT_TRUE([controller shouldShowIcon] != - [controller shouldShowAudioIndicator]); + EXPECT_FALSE([controller shouldShowMediaIndicator]); + } break; default: EXPECT_LE(3, [controller iconCapacity]); EXPECT_TRUE([controller shouldShowIcon]); - if ([controller projecting]) - EXPECT_FALSE([controller shouldShowAudioIndicator]); + if (indicatorState != TAB_MEDIA_STATE_NONE) + EXPECT_TRUE([controller shouldShowMediaIndicator]); else - EXPECT_TRUE(!![controller audioIndicatorView] == - [controller shouldShowAudioIndicator]); + EXPECT_FALSE([controller shouldShowMediaIndicator]); break; } } else { // Tab not selected/active and not mini tab. @@ -154,24 +163,26 @@ class TabControllerTest : public CocoaTest { case 0: EXPECT_FALSE([controller shouldShowCloseButton]); EXPECT_FALSE([controller shouldShowIcon]); - EXPECT_FALSE([controller shouldShowAudioIndicator]); + EXPECT_FALSE([controller shouldShowMediaIndicator]); break; case 1: EXPECT_FALSE([controller shouldShowCloseButton]); - if ([controller projecting]) + if (indicatorState == TAB_MEDIA_STATE_CAPTURING || + indicatorState == TAB_MEDIA_STATE_RECORDING) { + EXPECT_FALSE([controller shouldShowIcon]); + EXPECT_TRUE([controller shouldShowMediaIndicator]); + } else { EXPECT_TRUE([controller shouldShowIcon]); - else - EXPECT_TRUE([controller shouldShowIcon] != - [controller shouldShowAudioIndicator]); + EXPECT_FALSE([controller shouldShowMediaIndicator]); + } break; default: EXPECT_LE(2, [controller iconCapacity]); EXPECT_TRUE([controller shouldShowIcon]); - if ([controller projecting]) - EXPECT_FALSE([controller shouldShowAudioIndicator]); + if (indicatorState != TAB_MEDIA_STATE_NONE) + EXPECT_TRUE([controller shouldShowMediaIndicator]); else - EXPECT_TRUE(!![controller audioIndicatorView] == - [controller shouldShowAudioIndicator]); + EXPECT_FALSE([controller shouldShowMediaIndicator]); break; } } @@ -180,9 +191,8 @@ class TabControllerTest : public CocoaTest { EXPECT_TRUE([controller shouldShowIcon] == (!![controller iconView] && ![[controller iconView] isHidden])); EXPECT_TRUE([controller mini] == [[controller titleView] isHidden]); - EXPECT_TRUE([controller shouldShowAudioIndicator] == - (!![controller audioIndicatorView] && - ![[controller audioIndicatorView] isHidden])); + EXPECT_TRUE([controller shouldShowMediaIndicator] == + ![[controller mediaIndicatorView] isHidden]); EXPECT_TRUE([controller shouldShowCloseButton] != [[controller closeButton] isHidden]); @@ -198,22 +208,22 @@ class TabControllerTest : public CocoaTest { EXPECT_LE(NSMinY(tabFrame), NSMinY(iconFrame)); EXPECT_LE(NSMaxY(iconFrame), NSMaxY(tabFrame)); } - if ([controller shouldShowIcon] && [controller shouldShowAudioIndicator]) { + if ([controller shouldShowIcon] && [controller shouldShowMediaIndicator]) { EXPECT_LE(NSMaxX([[controller iconView] frame]), - NSMinX([[controller audioIndicatorView] frame])); + NSMinX([[controller mediaIndicatorView] frame])); } - if ([controller shouldShowAudioIndicator]) { - const NSRect audioIndicatorFrame = - [[controller audioIndicatorView] frame]; + if ([controller shouldShowMediaIndicator]) { + const NSRect mediaIndicatorFrame = + [[controller mediaIndicatorView] frame]; if (NSWidth(titleFrame) > 0) - EXPECT_LE(NSMaxX(titleFrame), NSMinX(audioIndicatorFrame)); - EXPECT_LE(NSMaxX(audioIndicatorFrame), NSMaxX(tabFrame)); - EXPECT_LE(NSMinY(tabFrame), NSMinY(audioIndicatorFrame)); - EXPECT_LE(NSMaxY(audioIndicatorFrame), NSMaxY(tabFrame)); + EXPECT_LE(NSMaxX(titleFrame), NSMinX(mediaIndicatorFrame)); + EXPECT_LE(NSMaxX(mediaIndicatorFrame), NSMaxX(tabFrame)); + EXPECT_LE(NSMinY(tabFrame), NSMinY(mediaIndicatorFrame)); + EXPECT_LE(NSMaxY(mediaIndicatorFrame), NSMaxY(tabFrame)); } - if ([controller shouldShowAudioIndicator] && + if ([controller shouldShowMediaIndicator] && [controller shouldShowCloseButton]) { - EXPECT_LE(NSMaxX([[controller audioIndicatorView] frame]), + EXPECT_LE(NSMaxX([[controller mediaIndicatorView] frame]), NSMinX([[controller closeButton] frame])); } if ([controller shouldShowCloseButton]) { @@ -489,64 +499,64 @@ TEST_F(TabControllerTest, TitleViewLayout) { // relevant combinations of tab state. This test overlaps with parts of the // other tests above. TEST_F(TabControllerTest, LayoutAndVisibilityOfSubviews) { + static const TabMediaState kMediaStatesToTest[] = { + TAB_MEDIA_STATE_NONE, TAB_MEDIA_STATE_CAPTURING, + TAB_MEDIA_STATE_AUDIO_PLAYING + }; + NSWindow* const window = test_window(); // Create TabController instance and place its view into the test window. base::scoped_nsobject<TabController> controller([[TabController alloc] init]); [[window contentView] addSubview:[controller view]]; - // Create favicon and audio indicator icon views. + // Create favicon and media indicator views. Disable animation in the media + // indicator view so that TabController's "what should be shown" logic can be + // tested effectively. If animations were left enabled, the + // shouldShowMediaIndicator method would return true during fade-out + // transitions. base::scoped_nsobject<NSImageView> faviconView( CreateImageViewFromResourceBundle(IDR_DEFAULT_FAVICON)); - base::scoped_nsobject<NSImageView> audioIndicatorView( - CreateImageViewFromResourceBundle(IDR_TAB_AUDIO_INDICATOR)); - - [controller setIconView:faviconView]; + base::scoped_nsobject<MediaIndicatorView> mediaIndicatorView( + [[MediaIndicatorView alloc] init]); + [mediaIndicatorView disableAnimations]; + [controller setMediaIndicatorView:mediaIndicatorView]; // Perform layout over all possible combinations, checking for correct // results. - for (int is_mini_tab = 0; is_mini_tab < 2; ++is_mini_tab) { - for (int is_active_tab = 0; is_active_tab < 2; ++is_active_tab) { - for (int is_audio_playing = 0; is_audio_playing < 2; ++is_audio_playing) { - for (int is_capturing = 0; is_capturing < 2; ++is_capturing) { - SCOPED_TRACE(::testing::Message() - << (is_active_tab ? "Active" : "Inactive") << ' ' - << (is_mini_tab ? "Mini " : "") - << "Tab with is_audio_playing=" << !!is_audio_playing - << " and is_capturing=" << !!is_capturing); - - // Simulate what tab_strip_controller would do to set up the - // TabController state. - [controller setMini:(is_mini_tab ? YES : NO)]; - [controller setActive:(is_active_tab ? YES : NO)]; - if (is_capturing) { - [controller setProjecting:YES]; - [controller setAudioIndicatorView:nil]; - } else { - [controller setProjecting:NO]; - if (is_audio_playing) - [controller setAudioIndicatorView:audioIndicatorView]; - else - [controller setAudioIndicatorView:nil]; - } - - // Test layout for every width from maximum to minimum. - NSRect tabFrame = [[controller view] frame]; - int min_width; - if (is_mini_tab) { - tabFrame.size.width = min_width = [TabController miniTabWidth]; - } else { - tabFrame.size.width = [TabController maxTabWidth]; - min_width = is_active_tab ? [TabController minSelectedTabWidth] : - [TabController minTabWidth]; - } - while (NSWidth(tabFrame) >= min_width) { - SCOPED_TRACE(::testing::Message() - << "width=" << tabFrame.size.width); - [[controller view] setFrame:tabFrame]; - CheckForExpectedLayoutAndVisibilityOfSubviews(controller); - --tabFrame.size.width; - } + for (int isMiniTab = 0; isMiniTab < 2; ++isMiniTab) { + for (int isActiveTab = 0; isActiveTab < 2; ++isActiveTab) { + for (size_t mediaStateIndex = 0; + mediaStateIndex < arraysize(kMediaStatesToTest); + ++mediaStateIndex) { + const TabMediaState mediaState = kMediaStatesToTest[mediaStateIndex]; + SCOPED_TRACE(::testing::Message() + << (isActiveTab ? "Active" : "Inactive") << ' ' + << (isMiniTab ? "Mini " : "") + << "Tab with media indicator state " << mediaState); + + // Simulate what tab_strip_controller would do to set up the + // TabController state. + [controller setMini:(isMiniTab ? YES : NO)]; + [controller setActive:(isActiveTab ? YES : NO)]; + [[controller mediaIndicatorView] updateIndicator:mediaState]; + [controller setIconView:faviconView]; + + // Test layout for every width from maximum to minimum. + NSRect tabFrame = [[controller view] frame]; + int minWidth; + if (isMiniTab) { + tabFrame.size.width = minWidth = [TabController miniTabWidth]; + } else { + tabFrame.size.width = [TabController maxTabWidth]; + minWidth = isActiveTab ? [TabController minSelectedTabWidth] : + [TabController minTabWidth]; + } + while (NSWidth(tabFrame) >= minWidth) { + SCOPED_TRACE(::testing::Message() << "width=" << tabFrame.size.width); + [[controller view] setFrame:tabFrame]; + CheckForExpectedLayoutAndVisibilityOfSubviews(controller); + --tabFrame.size.width; } } } diff --git a/chrome/browser/ui/cocoa/tabs/tab_projecting_image_view.h b/chrome/browser/ui/cocoa/tabs/tab_projecting_image_view.h deleted file mode 100644 index ef55d15..0000000 --- a/chrome/browser/ui/cocoa/tabs/tab_projecting_image_view.h +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) 2012 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#ifndef CHROME_BROWSER_UI_COCOA_TABS_TAB_PROJECTING_IMAGE_VIEW_H_ -#define CHROME_BROWSER_UI_COCOA_TABS_TAB_PROJECTING_IMAGE_VIEW_H_ - -#import <Cocoa/Cocoa.h> - -#include "base/mac/scoped_nsobject.h" -#include "base/memory/scoped_ptr.h" -#include "chrome/browser/ui/cocoa/tabs/throbbing_image_view.h" - -// ImageView for when a tab is in "projecting" mode. The view is made up of -// three images: background image (original favicon), projector sheet and an -// animated glow. This view paints outside the favicon bounds due to the glow. -@interface TabProjectingImageView : ThrobbingImageView { - @private - base::scoped_nsobject<NSImage> projectorImage_; -} - -- (id)initWithFrame:(NSRect)rect - backgroundImage:(NSImage*)backgroundImage - projectorImage:(NSImage*)projectorImage - throbImage:(NSImage*)throbImage - animationContainer:(gfx::AnimationContainer*)animationContainer; - -@end - -#endif // CHROME_BROWSER_UI_COCOA_TABS_TAB_PROJECTING_IMAGE_VIEW_H_ diff --git a/chrome/browser/ui/cocoa/tabs/tab_projecting_image_view.mm b/chrome/browser/ui/cocoa/tabs/tab_projecting_image_view.mm deleted file mode 100644 index 2f5d685..0000000 --- a/chrome/browser/ui/cocoa/tabs/tab_projecting_image_view.mm +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) 2012 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. - -#import "chrome/browser/ui/cocoa/tabs/tab_projecting_image_view.h" - -#include "ui/gfx/animation/animation.h" - -@implementation TabProjectingImageView - -- (id)initWithFrame:(NSRect)rect - backgroundImage:(NSImage*)backgroundImage - projectorImage:(NSImage*)projectorImage - throbImage:(NSImage*)throbImage - animationContainer:(gfx::AnimationContainer*)animationContainer { - if ((self = [super initWithFrame:rect - backgroundImage:backgroundImage - throbImage:throbImage - throbPosition:kThrobPositionOverlay - animationContainer:animationContainer])) { - projectorImage_.reset([projectorImage retain]); - } - return self; -} - -- (void)drawRect:(NSRect)rect { - // For projecting mode, we need to draw 3 centered icons of different sizes: - // - glow: 32x32 - // - projection sheet: 16x16 - // - favicon: 12x12 (0.75*16) - // Our bounds should be set to 32x32. - NSRect bounds = [self bounds]; - - const int faviconWidthAndHeight = bounds.size.width / 2 * 0.75; - const int faviconX = (bounds.size.width - faviconWidthAndHeight) / 2; - // Adjustment in y direction because projector screen is thinner at top. - const int faviconY = faviconX + 1; - [backgroundImage_ drawInRect:NSMakeRect(faviconX, - faviconY, - faviconWidthAndHeight, - faviconWidthAndHeight) - fromRect:NSZeroRect - operation:NSCompositeSourceOver - fraction:1]; - - const int projectorWidthAndHeight = bounds.size.width / 2; - const int projectorXY = (bounds.size.width - projectorWidthAndHeight) / 2; - [projectorImage_ drawInRect:NSMakeRect(projectorXY, - projectorXY, - projectorWidthAndHeight, - projectorWidthAndHeight) - fromRect:NSZeroRect - operation:NSCompositeSourceOver - fraction:1]; - - [throbImage_ drawInRect:[self bounds] - fromRect:NSZeroRect - operation:NSCompositeSourceOver - fraction:throbAnimation_->GetCurrentValue()]; -} - -@end diff --git a/chrome/browser/ui/cocoa/tabs/tab_projecting_image_view_unittest.mm b/chrome/browser/ui/cocoa/tabs/tab_projecting_image_view_unittest.mm deleted file mode 100644 index caa49aa..0000000 --- a/chrome/browser/ui/cocoa/tabs/tab_projecting_image_view_unittest.mm +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) 2011 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. - -#import "chrome/browser/ui/cocoa/tabs/tab_projecting_image_view.h" - -#include "base/message_loop/message_loop.h" -#import "chrome/browser/ui/cocoa/cocoa_test_helper.h" -#include "testing/gtest/include/gtest/gtest.h" -#include "testing/platform_test.h" - -namespace { - -class TabProjectingImageViewTest : public CocoaTest { - public: - TabProjectingImageViewTest() { - base::scoped_nsobject<NSImage> backgroundImage( - [[NSImage alloc] initWithSize:NSMakeSize(16, 16)]); - [backgroundImage lockFocus]; - NSRectFill(NSMakeRect(0, 0, 16, 16)); - [backgroundImage unlockFocus]; - - base::scoped_nsobject<NSImage> projectorImage( - [[NSImage alloc] initWithSize:NSMakeSize(16, 16)]); - [projectorImage lockFocus]; - NSRectFill(NSMakeRect(0, 0, 16, 16)); - [projectorImage unlockFocus]; - - base::scoped_nsobject<NSImage> throbImage( - [[NSImage alloc] initWithSize:NSMakeSize(32, 32)]); - [throbImage lockFocus]; - NSRectFill(NSMakeRect(0, 0, 32, 32)); - [throbImage unlockFocus]; - - base::scoped_nsobject<TabProjectingImageView> view( - [[TabProjectingImageView alloc] initWithFrame:NSMakeRect(0, 0, 32, 32) - backgroundImage:backgroundImage - projectorImage:projectorImage - throbImage:throbImage - animationContainer:NULL]); - view_ = view.get(); - [[test_window() contentView] addSubview:view_]; - } - - base::MessageLoopForUI message_loop_; // Needed for gfx::ThrobAnimation. - TabProjectingImageView* view_; -}; - -TEST_VIEW(TabProjectingImageViewTest, view_) - -} // namespace diff --git a/chrome/browser/ui/cocoa/tabs/tab_strip_controller.h b/chrome/browser/ui/cocoa/tabs/tab_strip_controller.h index a87c5a5..203502e 100644 --- a/chrome/browser/ui/cocoa/tabs/tab_strip_controller.h +++ b/chrome/browser/ui/cocoa/tabs/tab_strip_controller.h @@ -27,9 +27,6 @@ class TabStripModel; namespace content { class WebContents; } -namespace gfx { -class AnimationContainer; -} // The interface for the tab strip controller's delegate. // Delegating TabStripModelObserverBridge's events (in lieu of directly @@ -142,8 +139,6 @@ class AnimationContainer; // Helper for performing tab selection as a result of dragging over a tab. scoped_ptr<HoverTabSelector> hoverTabSelector_; - - scoped_refptr<gfx::AnimationContainer> animationContainer_; } @property(nonatomic) CGFloat leftIndentForControls; diff --git a/chrome/browser/ui/cocoa/tabs/tab_strip_controller.mm b/chrome/browser/ui/cocoa/tabs/tab_strip_controller.mm index 911dd5d..364a353 100644 --- a/chrome/browser/ui/cocoa/tabs/tab_strip_controller.mm +++ b/chrome/browser/ui/cocoa/tabs/tab_strip_controller.mm @@ -36,14 +36,13 @@ #import "chrome/browser/ui/cocoa/new_tab_button.h" #import "chrome/browser/ui/cocoa/tab_contents/favicon_util_mac.h" #import "chrome/browser/ui/cocoa/tab_contents/tab_contents_controller.h" +#import "chrome/browser/ui/cocoa/tabs/media_indicator_view.h" #import "chrome/browser/ui/cocoa/tabs/tab_controller.h" -#import "chrome/browser/ui/cocoa/tabs/tab_projecting_image_view.h" #import "chrome/browser/ui/cocoa/tabs/tab_strip_drag_controller.h" #import "chrome/browser/ui/cocoa/tabs/tab_strip_model_observer_bridge.h" #import "chrome/browser/ui/cocoa/tabs/tab_strip_view.h" #import "chrome/browser/ui/cocoa/tabs/tab_view.h" #import "chrome/browser/ui/cocoa/tabs/throbber_view.h" -#import "chrome/browser/ui/cocoa/tabs/throbbing_image_view.h" #include "chrome/browser/ui/find_bar/find_bar.h" #include "chrome/browser/ui/find_bar/find_bar_controller.h" #include "chrome/browser/ui/find_bar/find_tab_helper.h" @@ -70,7 +69,6 @@ #include "ui/base/models/list_selection_model.h" #include "ui/base/resource/resource_bundle.h" #include "ui/base/theme_provider.h" -#include "ui/gfx/animation/animation_container.h" #include "ui/gfx/image/image.h" using content::OpenURLParams; @@ -103,9 +101,6 @@ const CGFloat kNewTabButtonOffset = 8.0; // Time (in seconds) in which tabs animate to their final position. const NSTimeInterval kAnimationDuration = 0.125; -// The width and height of the icon + glow for projecting mode. -const CGFloat kProjectingIconWidthAndHeight = 32.0; - // Helper class for doing NSAnimationContext calls that takes a bool to disable // all the work. Useful for code that wants to conditionally animate. class ScopedNSAnimationContextGroup { @@ -215,39 +210,6 @@ NSImage* ApplyMask(NSImage* image, NSImage* mask) { }) autorelease]; } -// Creates a modified favicon used for the recording case. The mask is used for -// making part of the favicon transparent. (The part where the recording dot -// later is drawn.) -NSImage* CreateMaskedFaviconForRecording(NSImage* image, - NSImage* mask, - NSImage* recImage) { - return [CreateImageWithSize([image size], ^(NSSize size) { - CGFloat width = size.width; - CGFloat height = size.height; - - [image drawAtPoint:NSZeroPoint - fromRect:NSMakeRect(0, 0, width, height) - operation:NSCompositeCopy - fraction:1.0]; - - NSSize maskSize = [mask size]; - NSSize recImageSize = [recImage size]; - CGFloat offsetFromRight = recImageSize.width + - (maskSize.width - recImageSize.width) / 2; - CGFloat offsetFromBottom = (maskSize.height - recImageSize.height) / 2; - - NSRect maskBounds; - maskBounds.origin.x = width - offsetFromRight; - maskBounds.origin.y = -offsetFromBottom; - maskBounds.size = maskSize; - - [mask drawInRect:maskBounds - fromRect:NSZeroRect - operation:NSCompositeDestinationOut - fraction:1.0]; - }) autorelease]; -} - // Paints |overlay| on top of |ground|. NSImage* Overlay(NSImage* ground, NSImage* overlay, CGFloat alpha) { DCHECK_EQ([ground size].width, [overlay size].width); @@ -274,8 +236,8 @@ NSImage* Overlay(NSImage* ground, NSImage* overlay, CGFloat alpha) { - (void)regenerateSubviewList; - (NSInteger)indexForContentsView:(NSView*)view; - (NSImageView*)iconImageViewForContents:(content::WebContents*)contents; -- (void)updateFaviconForContents:(content::WebContents*)contents - atIndex:(NSInteger)modelIndex; +- (void)updateIconsForContents:(content::WebContents*)contents + atIndex:(NSInteger)modelIndex; - (void)layoutTabsWithAnimation:(BOOL)animate regenerateSubviews:(BOOL)doUpdate; - (void)animationDidStopForController:(TabController*)controller @@ -494,7 +456,6 @@ NSImage* Overlay(NSImage* ground, NSImage* overlay, CGFloat alpha) { [[TabStripDragController alloc] initWithTabStripController:self]); tabContentsArray_.reset([[NSMutableArray alloc] init]); tabArray_.reset([[NSMutableArray alloc] init]); - animationContainer_ = new gfx::AnimationContainer; NSWindow* browserWindow = [view window]; // Important note: any non-tab subviews not added to |permanentSubviews_| @@ -1312,7 +1273,6 @@ NSImage* Overlay(NSImage* ground, NSImage* overlay, CGFloat alpha) { 0, -[[self class] defaultTabHeight])]; [self setTabTitle:newController withContents:contents]; - [newController setProjecting:chrome::ShouldShowProjectingIndicator(contents)]; // If a tab is being inserted, we can again use the entire tab strip width // for layout. @@ -1330,7 +1290,7 @@ NSImage* Overlay(NSImage* ground, NSImage* overlay, CGFloat alpha) { // dragging a tab out into a new window, we have to put the tab's favicon // into the right state up front as we won't be told to do it from anywhere // else. - [self updateFaviconForContents:contents atIndex:modelIndex]; + [self updateIconsForContents:contents atIndex:modelIndex]; } // Called before |contents| is deactivated. @@ -1563,8 +1523,8 @@ NSImage* Overlay(NSImage* ground, NSImage* overlay, CGFloat alpha) { // Updates the current loading state, replacing the icon view with a favicon, // a throbber, the default icon, or nothing at all. -- (void)updateFaviconForContents:(content::WebContents*)contents - atIndex:(NSInteger)modelIndex { +- (void)updateIconsForContents:(content::WebContents*)contents + atIndex:(NSInteger)modelIndex { if (!contents) return; @@ -1612,63 +1572,19 @@ NSImage* Overlay(NSImage* ground, NSImage* overlay, CGFloat alpha) { if (newState == kTabDone || oldState != newState || oldHasIcon != newHasIcon) { NSView* iconView = nil; - NSImageView* audioIndicatorView = nil; if (newHasIcon) { if (newState == kTabDone) { - NSImageView* imageView = [self iconImageViewForContents:contents]; - - ui::ThemeProvider* theme = [[tabStripView_ window] themeProvider]; - if (theme && [tabController projecting]) { - NSImage* projectorGlow = - theme->GetNSImageNamed(IDR_TAB_CAPTURE_GLOW); - NSImage* projector = theme->GetNSImageNamed(IDR_TAB_CAPTURE); - - NSRect frame = NSMakeRect(0, - 0, - kProjectingIconWidthAndHeight, - kProjectingIconWidthAndHeight); - TabProjectingImageView* projectingView = - [[[TabProjectingImageView alloc] - initWithFrame:frame - backgroundImage:[imageView image] - projectorImage:projector - throbImage:projectorGlow - animationContainer:animationContainer_.get()] autorelease]; - - iconView = projectingView; - } else if (theme && chrome::ShouldShowRecordingIndicator(contents)) { - // Create a masked favicon. - NSImage* mask = theme->GetNSImageNamed(IDR_TAB_RECORDING_MASK); - NSImage* recording = theme->GetNSImageNamed(IDR_TAB_RECORDING); - NSImage* favIconMasked = CreateMaskedFaviconForRecording( - [imageView image], mask, recording); - - NSRect frame = - NSMakeRect(0, 0, kIconWidthAndHeight, kIconWidthAndHeight); - ThrobbingImageView* recordingView = - [[[ThrobbingImageView alloc] - initWithFrame:frame - backgroundImage:favIconMasked - throbImage:recording - throbPosition:kThrobPositionBottomRight - animationContainer:animationContainer_.get()] autorelease]; - - iconView = recordingView; - } else { - iconView = imageView; - - if (theme && chrome::IsPlayingAudio(contents)) { - NSImage* const image = - theme->GetNSImageNamed(IDR_TAB_AUDIO_INDICATOR); - if (image) { - NSRect frame; - frame.size = [image size]; - audioIndicatorView = - [[[NSImageView alloc] initWithFrame:frame] autorelease]; - [audioIndicatorView setImage:image]; - } - } + iconView = [self iconImageViewForContents:contents]; + const TabMediaState mediaState = + chrome::GetTabMediaStateForContents(contents); + // Create MediaIndicatorView upon first use. + if (mediaState != TAB_MEDIA_STATE_NONE && + ![tabController mediaIndicatorView]) { + MediaIndicatorView* const mediaIndicatorView = + [[[MediaIndicatorView alloc] init] autorelease]; + [tabController setMediaIndicatorView:mediaIndicatorView]; } + [[tabController mediaIndicatorView] updateIndicator:mediaState]; } else if (newState == kTabCrashed) { NSImage* oldImage = [[self iconImageViewForContents:contents] image]; NSRect frame = @@ -1676,6 +1592,8 @@ NSImage* Overlay(NSImage* ground, NSImage* overlay, CGFloat alpha) { iconView = [ThrobberView toastThrobberViewWithFrame:frame beforeImage:oldImage afterImage:sadFaviconImage]; + [[tabController mediaIndicatorView] + updateIndicator:TAB_MEDIA_STATE_NONE]; } else { NSRect frame = NSMakeRect(0, 0, kIconWidthAndHeight, kIconWidthAndHeight); @@ -1685,7 +1603,7 @@ NSImage* Overlay(NSImage* ground, NSImage* overlay, CGFloat alpha) { } [tabController setIconView:iconView]; - if (iconView && ![tabController projecting]) { + if (iconView) { // See the comment above kTabOverlap for why these DCHECKs exist. DCHECK_GE(NSMinX([iconView frame]), kTabOverlap); // TODO(thakis): Ideally, this would be true too, but it's not true in @@ -1693,7 +1611,6 @@ NSImage* Overlay(NSImage* ground, NSImage* overlay, CGFloat alpha) { //DCHECK_LE(NSMaxX([iconView frame]), // NSWidth([[tabController view] frame]) - kTabOverlap); } - [tabController setAudioIndicatorView:audioIndicatorView]; } } @@ -1719,9 +1636,8 @@ NSImage* Overlay(NSImage* ground, NSImage* overlay, CGFloat alpha) { if (change != TabStripModelObserver::LOADING_ONLY) [self setTabTitle:tabController withContents:contents]; - [tabController setProjecting:chrome::ShouldShowProjectingIndicator(contents)]; - [self updateFaviconForContents:contents atIndex:modelIndex]; + [self updateIconsForContents:contents atIndex:modelIndex]; TabContentsController* updatedController = [tabContentsArray_ objectAtIndex:index]; @@ -1776,7 +1692,7 @@ NSImage* Overlay(NSImage* ground, NSImage* overlay, CGFloat alpha) { [tabController setPinned:tabStripModel_->IsTabPinned(modelIndex)]; [tabController setApp:tabStripModel_->IsAppTab(modelIndex)]; [tabController setUrl:contents->GetURL()]; - [self updateFaviconForContents:contents atIndex:modelIndex]; + [self updateIconsForContents:contents atIndex:modelIndex]; // If the tab is being restored and it's pinned, the mini state is set after // the tab has already been rendered, so re-layout the tabstrip. In all other // cases, the state is set before the tab is rendered so this isn't needed. diff --git a/chrome/browser/ui/cocoa/tabs/throbbing_image_view.h b/chrome/browser/ui/cocoa/tabs/throbbing_image_view.h deleted file mode 100644 index 075d941..0000000 --- a/chrome/browser/ui/cocoa/tabs/throbbing_image_view.h +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) 2012 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#ifndef CHROME_BROWSER_UI_COCOA_TABS_THROBBING_IMAGE_VIEW_H_ -#define CHROME_BROWSER_UI_COCOA_TABS_THROBBING_IMAGE_VIEW_H_ - -#import <Cocoa/Cocoa.h> - -#include "base/mac/scoped_nsobject.h" -#include "base/memory/scoped_ptr.h" - -class ThrobbingImageViewAnimationDelegate; - -namespace gfx { -class Animation; -class AnimationContainer; -} // namespace gfx - -// Where to position the throb image. For the overlay position, the throb image -// will be drawn with the same size as the background image. For the bottom -// right position, it will have its original size. -enum ThrobPosition { - kThrobPositionOverlay, - kThrobPositionBottomRight -}; - -@interface ThrobbingImageView : NSView { - @protected - base::scoped_nsobject<NSImage> backgroundImage_; - base::scoped_nsobject<NSImage> throbImage_; - scoped_ptr<gfx::Animation> throbAnimation_; - - @private - scoped_ptr<ThrobbingImageViewAnimationDelegate> delegate_; - ThrobPosition throbPosition_; -} - -- (id)initWithFrame:(NSRect)rect - backgroundImage:(NSImage*)backgroundImage - throbImage:(NSImage*)throbImage - throbPosition:(ThrobPosition)throbPosition - animationContainer:(gfx::AnimationContainer*)animationContainer; - -@end - -#endif // CHROME_BROWSER_UI_COCOA_TABS_THROBBING_IMAGE_VIEW_H_ diff --git a/chrome/browser/ui/cocoa/tabs/throbbing_image_view.mm b/chrome/browser/ui/cocoa/tabs/throbbing_image_view.mm deleted file mode 100644 index e5de04ac..0000000 --- a/chrome/browser/ui/cocoa/tabs/throbbing_image_view.mm +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) 2012 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. - -#import "chrome/browser/ui/cocoa/tabs/throbbing_image_view.h" - -#include "chrome/browser/ui/tabs/tab_utils.h" -#include "ui/gfx/animation/animation.h" -#include "ui/gfx/animation/animation_delegate.h" - -class ThrobbingImageViewAnimationDelegate : public gfx::AnimationDelegate { - public: - ThrobbingImageViewAnimationDelegate(NSView* view) : view_(view) {} - virtual void AnimationProgressed(const gfx::Animation* animation) OVERRIDE { - [view_ setNeedsDisplay:YES]; - } - private: - NSView* view_; // weak -}; - -@implementation ThrobbingImageView - -- (id)initWithFrame:(NSRect)rect - backgroundImage:(NSImage*)backgroundImage - throbImage:(NSImage*)throbImage - throbPosition:(ThrobPosition)throbPosition - animationContainer:(gfx::AnimationContainer*)animationContainer { - if ((self = [super initWithFrame:rect])) { - backgroundImage_.reset([backgroundImage retain]); - throbImage_.reset([throbImage retain]); - - delegate_.reset(new ThrobbingImageViewAnimationDelegate(self)); - - throbAnimation_ = chrome::CreateTabRecordingIndicatorAnimation(); - throbAnimation_->set_delegate(delegate_.get()); - throbAnimation_->SetContainer(animationContainer); - throbAnimation_->Start(); - - throbPosition_ = throbPosition; - } - return self; -} - -- (void)dealloc { - throbAnimation_->Stop(); - [super dealloc]; -} - -- (void)drawRect:(NSRect)rect { - [backgroundImage_ drawInRect:[self bounds] - fromRect:NSZeroRect - operation:NSCompositeSourceOver - fraction:1]; - - NSRect b = [self bounds]; - NSRect throbImageBounds; - if (throbPosition_ == kThrobPositionBottomRight) { - NSSize throbImageSize = [throbImage_ size]; - throbImageBounds.origin = b.origin; - throbImageBounds.origin.x += NSWidth(b) - throbImageSize.width; - throbImageBounds.size = throbImageSize; - } else { - throbImageBounds = b; - } - [throbImage_ drawInRect:throbImageBounds - fromRect:NSZeroRect - operation:NSCompositeSourceOver - fraction:throbAnimation_->GetCurrentValue()]; -} - -@end diff --git a/chrome/browser/ui/cocoa/tabs/throbbing_image_view_unittest.mm b/chrome/browser/ui/cocoa/tabs/throbbing_image_view_unittest.mm deleted file mode 100644 index 5ce2b13..0000000 --- a/chrome/browser/ui/cocoa/tabs/throbbing_image_view_unittest.mm +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) 2011 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. - -#import "chrome/browser/ui/cocoa/tabs/throbbing_image_view.h" - -#include "base/message_loop/message_loop.h" -#import "chrome/browser/ui/cocoa/cocoa_test_helper.h" -#include "testing/gtest/include/gtest/gtest.h" -#include "testing/platform_test.h" - -namespace { - -class ThrobbingImageViewTest : public CocoaTest { - public: - ThrobbingImageViewTest() { - base::scoped_nsobject<NSImage> image( - [[NSImage alloc] initWithSize:NSMakeSize(16, 16)]); - [image lockFocus]; - NSRectFill(NSMakeRect(0, 0, 16, 16)); - [image unlockFocus]; - - base::scoped_nsobject<ThrobbingImageView> view( - [[ThrobbingImageView alloc] initWithFrame:NSMakeRect(0, 0, 16, 16) - backgroundImage:image - throbImage:image - throbPosition:kThrobPositionOverlay - animationContainer:NULL]); - view_ = view.get(); - [[test_window() contentView] addSubview:view_]; - } - - base::MessageLoopForUI message_loop_; // Needed for gfx::ThrobAnimation. - ThrobbingImageView* view_; -}; - -TEST_VIEW(ThrobbingImageViewTest, view_) - -} // namespace - diff --git a/chrome/browser/ui/gtk/tabs/tab_renderer_gtk.cc b/chrome/browser/ui/gtk/tabs/tab_renderer_gtk.cc index 4835970..fc86ca2 100644 --- a/chrome/browser/ui/gtk/tabs/tab_renderer_gtk.cc +++ b/chrome/browser/ui/gtk/tabs/tab_renderer_gtk.cc @@ -91,21 +91,6 @@ const int kMiniTitleChangeThrobDuration = 1000; // The horizontal offset used to position the close button in the tab. const int kCloseButtonHorzFuzz = 4; -// Scale to resize the current favicon by when projecting. -const double kProjectingFaviconResizeScale = 0.75; - -// Scale to translate the current favicon by to center after scaling. -const double kProjectingFaviconXShiftScale = 0.15; - -// Scale to translate the current favicon by to center after scaling. -const double kProjectingFaviconYShiftScale = 0.1; - -// Scale to resize the projection sheet glow by. -const double kProjectingGlowResizeScale = 2.0; - -// Scale to translate the current glow by in the negative X and Y directions. -const double kProjectingGlowShiftScale = 0.5; - // Gets the bounds of |widget| relative to |parent|. gfx::Rect GetWidgetBoundsRelativeToParent(GtkWidget* parent, GtkWidget* widget) { @@ -266,7 +251,8 @@ TabRendererGtk::TabData::TabData() blocked(false), animating_mini_change(false), app(false), - capture_state(NONE) { + media_state(TAB_MEDIA_STATE_NONE), + previous_media_state(TAB_MEDIA_STATE_NONE) { } TabRendererGtk::TabData::~TabData() {} @@ -315,9 +301,11 @@ class TabRendererGtk::FaviconCrashAnimation : public gfx::LinearAnimation, TabRendererGtk::TabRendererGtk(GtkThemeService* theme_service) : showing_icon_(false), + showing_media_indicator_(false), showing_close_button_(false), favicon_hiding_offset_(0), should_display_crashed_favicon_(false), + animating_media_state_(TAB_MEDIA_STATE_NONE), loading_animation_(theme_service), background_offset_x_(0), background_offset_y_(kInactiveTabBackgroundOffsetY), @@ -369,15 +357,18 @@ void TabRendererGtk::UpdateData(WebContents* contents, if (!loading_only) { data_.title = contents->GetTitle(); data_.incognito = contents->GetBrowserContext()->IsOffTheRecord(); - data_.crashed = contents->IsCrashed(); - // Set whether we are recording or capturing tab media for this tab. - if (chrome::ShouldShowProjectingIndicator(contents)) { - data_.capture_state = PROJECTING; - } else if (chrome::ShouldShowRecordingIndicator(contents)) { - data_.capture_state = RECORDING; + TabMediaState next_media_state; + if (contents->IsCrashed()) { + data_.crashed = true; + next_media_state = TAB_MEDIA_STATE_NONE; } else { - data_.capture_state = NONE; + data_.crashed = false; + next_media_state = chrome::GetTabMediaStateForContents(contents); + } + if (data_.media_state != next_media_state) { + data_.previous_media_state = data_.media_state; + data_.media_state = next_media_state; } SkBitmap* app_icon = @@ -399,12 +390,8 @@ void TabRendererGtk::UpdateData(WebContents* contents, // For source images smaller than the favicon square, scale them as if // they were padded to fit the favicon square, so we don't blow up tiny // favicons into larger or nonproportional results. - int icon_size = gfx::kFaviconSize; - if (data_.capture_state == PROJECTING) - icon_size *= kProjectingFaviconResizeScale; - GdkPixbuf* pixbuf = GetResizedGdkPixbufFromSkBitmap(data_.favicon, - icon_size, icon_size); + gfx::kFaviconSize, gfx::kFaviconSize); data_.cairo_favicon.UsePixbuf(pixbuf); g_object_unref(pixbuf); } else { @@ -420,8 +407,6 @@ void TabRendererGtk::UpdateData(WebContents* contents, (data_.favicon.pixelRef() == ui::ResourceBundle::GetSharedInstance().GetImageNamed( IDR_DEFAULT_FAVICON).AsBitmap().pixelRef()); - - UpdateFaviconOverlay(contents); } // Loading state also involves whether we show the favicon, since that's where @@ -443,6 +428,13 @@ void TabRendererGtk::UpdateFromModel() { StopCrashAnimation(); ResetCrashedFavicon(); } + + if (data_.media_state != data_.previous_media_state) { + data_.previous_media_state = data_.media_state; + if (data_.media_state != TAB_MEDIA_STATE_NONE) + animating_media_state_ = data_.media_state; + StartMediaIndicatorAnimation(); + } } void TabRendererGtk::SetBlocked(bool blocked) { @@ -543,17 +535,16 @@ void TabRendererGtk::PaintFaviconArea(GtkWidget* widget, cairo_t* cr) { PaintIcon(widget, cr); } -bool TabRendererGtk::ShouldShowIcon() const { - if (mini() && height() >= GetMinimumUnselectedSize().height()) { - return true; - } else if (!data_.show_icon) { - return false; - } else if (IsActive()) { - // The active tab clips favicon before close button. - return IconCapacity() >= 2; - } - // Non-selected tabs clip close button before favicon. - return IconCapacity() >= 1; +void TabRendererGtk::MaybeAdjustLeftForMiniTab(gfx::Rect* icon_bounds) const { + if (!(mini() || data_.animating_mini_change) || + bounds_.width() >= kMiniTabRendererAsNormalTabWidth) + return; + const int mini_delta = kMiniTabRendererAsNormalTabWidth - GetMiniWidth(); + const int ideal_delta = bounds_.width() - GetMiniWidth(); + const int ideal_x = (GetMiniWidth() - icon_bounds->width()) / 2; + icon_bounds->set_x(icon_bounds->x() + static_cast<int>( + (1 - static_cast<float>(ideal_delta) / static_cast<float>(mini_delta)) * + (ideal_x - icon_bounds->x()))); } // static @@ -650,10 +641,14 @@ void TabRendererGtk::AnimationProgressed(const gfx::Animation* animation) { } void TabRendererGtk::AnimationCanceled(const gfx::Animation* animation) { + if (media_indicator_animation_ == animation) + animating_media_state_ = data_.media_state; AnimationEnded(animation); } void TabRendererGtk::AnimationEnded(const gfx::Animation* animation) { + if (media_indicator_animation_ == animation) + animating_media_state_ = data_.media_state; gtk_widget_queue_draw(tab_.get()); } @@ -677,6 +672,13 @@ bool TabRendererGtk::IsPerformingCrashAnimation() const { return crash_animation_.get() && crash_animation_->is_animating(); } +void TabRendererGtk::StartMediaIndicatorAnimation() { + media_indicator_animation_ = + chrome::CreateTabMediaIndicatorFadeAnimation(data_.media_state); + media_indicator_animation_->set_delegate(this); + media_indicator_animation_->Start(); +} + void TabRendererGtk::SetFaviconHidingOffset(int offset) { favicon_hiding_offset_ = offset; SchedulePaint(); @@ -690,37 +692,6 @@ void TabRendererGtk::ResetCrashedFavicon() { should_display_crashed_favicon_ = false; } -void TabRendererGtk::UpdateFaviconOverlay(WebContents* contents) { - if (data_.capture_state != NONE) { - gfx::Image recording = theme_service_->GetImageNamed( - data_.capture_state == PROJECTING ? - IDR_TAB_CAPTURE_GLOW : IDR_TAB_RECORDING); - - int icon_size = data_.capture_state == PROJECTING ? - gfx::kFaviconSize * kProjectingGlowResizeScale : - recording.ToImageSkia()->width(); - - GdkPixbuf* pixbuf = data_.favicon.isNull() ? - gfx::GdkPixbufFromSkBitmap(*recording.ToSkBitmap()) : - GetResizedGdkPixbufFromSkBitmap(*recording.ToSkBitmap(), - icon_size, icon_size); - data_.cairo_overlay.UsePixbuf(pixbuf); - g_object_unref(pixbuf); - - if (!favicon_overlay_animation_.get()) { - favicon_overlay_animation_ = - chrome::CreateTabRecordingIndicatorAnimation(); - favicon_overlay_animation_->set_delegate(this); - } - if (!favicon_overlay_animation_->is_animating()) - favicon_overlay_animation_->Start(); - } else { - data_.cairo_overlay.Reset(); - if (favicon_overlay_animation_.get()) - favicon_overlay_animation_.reset(); - } -} - void TabRendererGtk::Paint(GtkWidget* widget, cairo_t* cr) { // Don't paint if we're narrower than we can render correctly. (This should // only happen during animations). @@ -729,8 +700,10 @@ void TabRendererGtk::Paint(GtkWidget* widget, cairo_t* cr) { // See if the model changes whether the icons should be painted. const bool show_icon = ShouldShowIcon(); + const bool show_media_indicator = ShouldShowMediaIndicator(); const bool show_close_button = ShouldShowCloseBox(); if (show_icon != showing_icon_ || + show_media_indicator != showing_media_indicator_ || show_close_button != showing_close_button_) Layout(); @@ -741,6 +714,9 @@ void TabRendererGtk::Paint(GtkWidget* widget, cairo_t* cr) { if (show_icon) PaintIcon(widget, cr); + + if (show_media_indicator) + PaintMediaIndicator(widget, cr); } cairo_surface_t* TabRendererGtk::PaintToSurface(GtkWidget* widget, @@ -781,19 +757,7 @@ void TabRendererGtk::Layout() { int favicon_top = kTopPadding + (content_height - gfx::kFaviconSize) / 2; favicon_bounds_.SetRect(local_bounds.x(), favicon_top, gfx::kFaviconSize, gfx::kFaviconSize); - if ((mini() || data_.animating_mini_change) && - bounds_.width() < kMiniTabRendererAsNormalTabWidth) { - int mini_delta = kMiniTabRendererAsNormalTabWidth - GetMiniWidth(); - int ideal_delta = bounds_.width() - GetMiniWidth(); - if (ideal_delta < mini_delta) { - int ideal_x = (GetMiniWidth() - gfx::kFaviconSize) / 2; - int x = favicon_bounds_.x() + static_cast<int>( - (1 - static_cast<float>(ideal_delta) / - static_cast<float>(mini_delta)) * - (ideal_x - favicon_bounds_.x())); - favicon_bounds_.set_x(x); - } - } + MaybeAdjustLeftForMiniTab(&favicon_bounds_); } else { favicon_bounds_.SetRect(local_bounds.x(), local_bounds.y(), 0, 0); } @@ -826,6 +790,23 @@ void TabRendererGtk::Layout() { close_button_bounds_.SetRect(0, 0, 0, 0); } + showing_media_indicator_ = ShouldShowMediaIndicator(); + if (showing_media_indicator_) { + const gfx::Image& media_indicator_image = + chrome::GetTabMediaIndicatorImage(animating_media_state_); + media_indicator_bounds_.set_width(media_indicator_image.Width()); + media_indicator_bounds_.set_height(media_indicator_image.Height()); + media_indicator_bounds_.set_y( + kTopPadding + (content_height - media_indicator_bounds_.height()) / 2); + const int right = showing_close_button_ ? + close_button_bounds_.x() : local_bounds.right(); + media_indicator_bounds_.set_x(std::max( + local_bounds.x(), right - media_indicator_bounds_.width())); + MaybeAdjustLeftForMiniTab(&media_indicator_bounds_); + } else { + media_indicator_bounds_.SetRect(local_bounds.x(), local_bounds.y(), 0, 0); + } + if (!mini() || width() >= kMiniTabRendererAsNormalTabWidth) { // Size the Title text to fill the remaining space. int title_left = favicon_bounds_.right() + kFaviconTitleSpacing; @@ -840,17 +821,23 @@ void TabRendererGtk::Layout() { title_top -= (text_height - minimum_size.height()) / 2; int title_width; - if (close_button_bounds_.width() && close_button_bounds_.height()) { - title_width = std::max(close_button_bounds_.x() - - kTitleCloseButtonSpacing - title_left, 0); + if (showing_media_indicator_) { + title_width = media_indicator_bounds_.x() - kTitleCloseButtonSpacing - + title_left; + } else if (close_button_bounds_.width() && close_button_bounds_.height()) { + title_width = close_button_bounds_.x() - kTitleCloseButtonSpacing - + title_left; } else { - title_width = std::max(local_bounds.width() - title_left, 0); + title_width = local_bounds.width() - title_left; } + title_width = std::max(title_width, 0); title_bounds_.SetRect(title_left, title_top, title_width, content_height); } favicon_bounds_.set_x( gtk_util::MirroredLeftPointForRect(tab_.get(), favicon_bounds_)); + media_indicator_bounds_.set_x( + gtk_util::MirroredLeftPointForRect(tab_.get(), media_indicator_bounds_)); close_button_bounds_.set_x( gtk_util::MirroredLeftPointForRect(tab_.get(), close_button_bounds_)); title_bounds_.set_x( @@ -933,78 +920,25 @@ void TabRendererGtk::PaintIcon(GtkWidget* widget, cairo_t* cr) { } if (to_display) { - int favicon_x = favicon_bounds_.x(); - int favicon_y = favicon_bounds_.y() + favicon_hiding_offset_; - if (data_.capture_state == PROJECTING) { - favicon_x += favicon_bounds_.width() * kProjectingFaviconXShiftScale; - favicon_y += favicon_bounds_.height() * kProjectingFaviconYShiftScale; - } - - to_display->SetSource(cr, widget, favicon_x, favicon_y); + to_display->SetSource(cr, widget, favicon_bounds_.x(), + favicon_bounds_.y() + favicon_hiding_offset_); cairo_paint(cr); } +} - if (data_.cairo_overlay.valid() && favicon_overlay_animation_.get()) { - if (data_.capture_state == PROJECTING) { - theme_service_->GetImageNamed(IDR_TAB_CAPTURE).ToCairo()-> - SetSource(cr, - widget, - favicon_bounds_.x(), - favicon_bounds_.y() + favicon_hiding_offset_); - cairo_paint(cr); - } else if (data_.capture_state == RECORDING) { - // Add mask around the recording overlay image (red dot). - gfx::CairoCachedSurface* tab_bg; - if (IsActive()) { - tab_bg = theme_service_->GetImageNamed(IDR_THEME_TOOLBAR).ToCairo(); - } else { - int theme_id = data_.incognito ? - IDR_THEME_TAB_BACKGROUND_INCOGNITO : IDR_THEME_TAB_BACKGROUND; - tab_bg = theme_service_->GetImageNamed(theme_id).ToCairo(); - } - tab_bg->SetSource(cr, widget, -background_offset_x_, 0); - cairo_pattern_set_extend(cairo_get_source(cr), CAIRO_EXTEND_REPEAT); - - gfx::CairoCachedSurface* recording_mask = - theme_service_->GetImageNamed(IDR_TAB_RECORDING_MASK).ToCairo(); - int offset_from_right = data_.cairo_overlay.Width() + - (recording_mask->Width() - data_.cairo_overlay.Width()) / 2; - int favicon_x = favicon_bounds_.x() + favicon_bounds_.width() - - offset_from_right; - int offset_from_bottom = data_.cairo_overlay.Height() + - (recording_mask->Height() - data_.cairo_overlay.Height()) / 2; - int favicon_y = favicon_bounds_.y() + favicon_hiding_offset_ + - favicon_bounds_.height() - offset_from_bottom; - recording_mask->MaskSource(cr, widget, favicon_x, favicon_y); - - if (!IsActive()) { - double throb_value = GetThrobValue(); - if (throb_value > 0) { - cairo_push_group(cr); - gfx::CairoCachedSurface* active_bg = - theme_service_->GetImageNamed(IDR_THEME_TOOLBAR).ToCairo(); - active_bg->SetSource(cr, widget, -background_offset_x_, 0); - cairo_pattern_set_extend(cairo_get_source(cr), CAIRO_EXTEND_REPEAT); - recording_mask->MaskSource(cr, widget, favicon_x, favicon_y); - cairo_pop_group_to_source(cr); - cairo_paint_with_alpha(cr, throb_value); - } - } - } +void TabRendererGtk::PaintMediaIndicator(GtkWidget* widget, cairo_t* cr) { + if (media_indicator_bounds_.IsEmpty() || !media_indicator_animation_) + return; - int favicon_x = favicon_bounds_.x(); - int favicon_y = favicon_bounds_.y() + favicon_hiding_offset_; - if (data_.capture_state == PROJECTING) { - favicon_x -= favicon_bounds_.width() * kProjectingGlowShiftScale; - favicon_y -= favicon_bounds_.height() * kProjectingGlowShiftScale; - } else if (data_.capture_state == RECORDING) { - favicon_x += favicon_bounds_.width() - data_.cairo_overlay.Width(); - favicon_y += favicon_bounds_.height() - data_.cairo_overlay.Height(); - } + double opaqueness = media_indicator_animation_->GetCurrentValue(); + if (data_.media_state == TAB_MEDIA_STATE_NONE) + opaqueness = 1.0 - opaqueness; // Fading out, not in. - data_.cairo_overlay.SetSource(cr, widget, favicon_x, favicon_y); - cairo_paint_with_alpha(cr, favicon_overlay_animation_->GetCurrentValue()); - } + const gfx::Image& media_indicator_image = + chrome::GetTabMediaIndicatorImage(animating_media_state_); + media_indicator_image.ToCairo()->SetSource( + cr, widget, media_indicator_bounds_.x(), media_indicator_bounds_.y()); + cairo_paint_with_alpha(cr, opaqueness); } void TabRendererGtk::PaintTabBackground(GtkWidget* widget, cairo_t* cr) { @@ -1128,12 +1062,30 @@ void TabRendererGtk::PaintLoadingAnimation(GtkWidget* widget, int TabRendererGtk::IconCapacity() const { if (height() < GetMinimumUnselectedSize().height()) return 0; - return (width() - kLeftPadding - kRightPadding) / gfx::kFaviconSize; + const int available_width = + std::max(0, width() - kLeftPadding - kRightPadding); + const int kPaddingBetweenIcons = 2; + if (available_width >= gfx::kFaviconSize && + available_width < (gfx::kFaviconSize + kPaddingBetweenIcons)) { + return 1; + } + return available_width / (gfx::kFaviconSize + kPaddingBetweenIcons); +} + +bool TabRendererGtk::ShouldShowIcon() const { + return chrome::ShouldTabShowFavicon( + IconCapacity(), mini(), IsActive(), data_.show_icon, + animating_media_state_); +} + +bool TabRendererGtk::ShouldShowMediaIndicator() const { + return chrome::ShouldTabShowMediaIndicator( + IconCapacity(), mini(), IsActive(), data_.show_icon, + animating_media_state_); } bool TabRendererGtk::ShouldShowCloseBox() const { - // The selected tab never clips close button. - return !mini() && (IsActive() || IconCapacity() >= 3); + return chrome::ShouldTabShowCloseButton(IconCapacity(), mini(), IsActive()); } CustomDrawButton* TabRendererGtk::MakeCloseButton() { diff --git a/chrome/browser/ui/gtk/tabs/tab_renderer_gtk.h b/chrome/browser/ui/gtk/tabs/tab_renderer_gtk.h index a1b9fcc..01cd308 100644 --- a/chrome/browser/ui/gtk/tabs/tab_renderer_gtk.h +++ b/chrome/browser/ui/gtk/tabs/tab_renderer_gtk.h @@ -12,6 +12,7 @@ #include "base/compiler_specific.h" #include "base/memory/scoped_ptr.h" #include "base/strings/string16.h" +#include "chrome/browser/ui/tabs/tab_utils.h" #include "content/public/browser/notification_observer.h" #include "content/public/browser/notification_registrar.h" #include "third_party/skia/include/core/SkBitmap.h" @@ -176,6 +177,10 @@ class TabRendererGtk : public gfx::AnimationDelegate, // Returns whether the Tab should display a favicon. bool ShouldShowIcon() const; + // Invoked from Layout() to adjust the position of the favicon or media + // indicator for mini tabs. + void MaybeAdjustLeftForMiniTab(gfx::Rect* bounds) const; + // Returns the minimum possible size of a single unselected Tab. static gfx::Size GetMinimumUnselectedSize(); // Returns the minimum possible size of a selected Tab. Selected tabs must @@ -236,12 +241,6 @@ class TabRendererGtk : public gfx::AnimationDelegate, private: class FaviconCrashAnimation; - enum CaptureState { - NONE, - RECORDING, - PROJECTING - }; - // Model data. We store this here so that we don't need to ask the underlying // model, which is tricky since instances of this object can outlive the // corresponding objects in the underlying model. @@ -251,7 +250,6 @@ class TabRendererGtk : public gfx::AnimationDelegate, SkBitmap favicon; gfx::CairoCachedSurface cairo_favicon; - gfx::CairoCachedSurface cairo_overlay; bool is_default_favicon; string16 title; bool loading; @@ -262,7 +260,8 @@ class TabRendererGtk : public gfx::AnimationDelegate, bool blocked; bool animating_mini_change; bool app; - CaptureState capture_state; + TabMediaState media_state; + TabMediaState previous_media_state; }; // Overridden from gfx::AnimationDelegate: @@ -277,16 +276,16 @@ class TabRendererGtk : public gfx::AnimationDelegate, // Return true if the crash animation is currently running. bool IsPerformingCrashAnimation() const; + // Starts the media indicator fade-in/out animation. There's no stop method + // because this is not a continuous animation. + void StartMediaIndicatorAnimation(); + // Set the temporary offset for the favicon. This is used during animation. void SetFaviconHidingOffset(int offset); void DisplayCrashedFavicon(); void ResetCrashedFavicon(); - // Sets up an overlay for the favicon and starts a throbbing animation - // if this tab is currently capturing media. - void UpdateFaviconOverlay(content::WebContents* contents); - // Generates the bounds for the interior items of the tab. void Layout(); @@ -305,6 +304,7 @@ class TabRendererGtk : public gfx::AnimationDelegate, // Paint various portions of the Tab void PaintTitle(GtkWidget* widget, cairo_t* cr); void PaintIcon(GtkWidget* widget, cairo_t* cr); + void PaintMediaIndicator(GtkWidget* widget, cairo_t* cr); void PaintTabBackground(GtkWidget* widget, cairo_t* cr); void PaintInactiveTabBackground(GtkWidget* widget, cairo_t* cr); void PaintActiveTabBackground(GtkWidget* widget, cairo_t* cr); @@ -329,6 +329,9 @@ class TabRendererGtk : public gfx::AnimationDelegate, // current size. int IconCapacity() const; + // Returns whether the Tab should display the media indicator. + bool ShouldShowMediaIndicator() const; + // Returns whether the Tab should display a close button. bool ShouldShowCloseBox() const; @@ -360,6 +363,7 @@ class TabRendererGtk : public gfx::AnimationDelegate, // The bounds of various sections of the display. gfx::Rect favicon_bounds_; gfx::Rect title_bounds_; + gfx::Rect media_indicator_bounds_; gfx::Rect close_button_bounds_; TabData data_; @@ -384,6 +388,10 @@ class TabRendererGtk : public gfx::AnimationDelegate, // changes and layout appropriately. bool showing_icon_; + // Whether we're showing the media indicator. It is cached so that we can + // detect when it changes and layout appropriately. + bool showing_media_indicator_; + // Whether we are showing the close button. It is cached so that we can // detect when it changes and layout appropriately. bool showing_close_button_; @@ -410,8 +418,10 @@ class TabRendererGtk : public gfx::AnimationDelegate, // Animation used when the title of an inactive mini-tab changes. scoped_ptr<gfx::ThrobAnimation> mini_title_animation_; - // Animation used when the favicon has an overlay (e.g. for recording). - scoped_ptr<gfx::Animation> favicon_overlay_animation_; + // Media indicator fade-in/out animation (i.e., only on show/hide, not a + // continuous animation). + scoped_ptr<gfx::Animation> media_indicator_animation_; + TabMediaState animating_media_state_; // Contains the loading animation state. LoadingAnimation loading_animation_; diff --git a/chrome/browser/ui/tabs/tab_utils.cc b/chrome/browser/ui/tabs/tab_utils.cc index 4a63151..a43ce41 100644 --- a/chrome/browser/ui/tabs/tab_utils.cc +++ b/chrome/browser/ui/tabs/tab_utils.cc @@ -7,15 +7,25 @@ #include "chrome/browser/media/audio_stream_indicator.h" #include "chrome/browser/media/media_capture_devices_dispatcher.h" #include "chrome/browser/media/media_stream_capture_indicator.h" -#include "content/public/browser/render_process_host.h" -#include "content/public/browser/render_view_host.h" -#include "content/public/browser/web_contents.h" +#include "grit/theme_resources.h" +#include "ui/base/resource/resource_bundle.h" #include "ui/gfx/animation/multi_animation.h" namespace chrome { namespace { +// Interval between frame updates of the tab indicator animations. This is not +// the usual 60 FPS because a trade-off must be made between tab UI animation +// smoothness and media recording/playback performance on low-end hardware. +const int kIndicatorFrameIntervalMs = 50; // 20 FPS + +// Fade-in/out duration for the tab indicator animations. Fade-in is quick to +// immediately notify the user. Fade-out is more gradual, so that the user has +// a chance of finding a tab that has quickly "blipped" on and off. +const int kIndicatorFadeInDurationMs = 200; +const int kIndicatorFadeOutDurationMs = 1000; + // Animation that throbs in (towards 1.0) and out (towards 0.0), and ends in the // "in" state. class TabRecordingIndicatorAnimation : public gfx::MultiAnimation { @@ -32,17 +42,9 @@ class TabRecordingIndicatorAnimation : public gfx::MultiAnimation { const base::TimeDelta interval) : MultiAnimation(parts, interval) {} - // Throbbing fade in/out duration on "this web page is watching and/or - // listening to you" favicon overlay. - static const int kCaptureIndicatorCycleDurationMs = 1000; - // Number of times to "toggle throb" the recording and tab capture indicators // when they first appear. static const int kCaptureIndicatorThrobCycles = 5; - - // Interval between frame updates of the recording and tab capture indicator - // throb animations. - static const int kCaptureIndicatorFrameIntervalMs = 50; // 20 FPS }; double TabRecordingIndicatorAnimation::GetCurrentValue() const { @@ -58,10 +60,11 @@ TabRecordingIndicatorAnimation::Create() { must_be_odd_so_animation_finishes_in_showing_state); for (int i = 0; i < kCaptureIndicatorThrobCycles; ++i) { parts.push_back(MultiAnimation::Part( - kCaptureIndicatorCycleDurationMs, gfx::Tween::EASE_IN)); + i % 2 ? kIndicatorFadeOutDurationMs : kIndicatorFadeInDurationMs, + gfx::Tween::EASE_IN)); } const base::TimeDelta interval = - base::TimeDelta::FromMilliseconds(kCaptureIndicatorFrameIntervalMs); + base::TimeDelta::FromMilliseconds(kIndicatorFrameIntervalMs); scoped_ptr<TabRecordingIndicatorAnimation> animation( new TabRecordingIndicatorAnimation(parts, interval)); animation->set_continuous(false); @@ -70,47 +73,114 @@ TabRecordingIndicatorAnimation::Create() { } // namespace -bool ShouldShowProjectingIndicator(content::WebContents* contents) { - scoped_refptr<MediaStreamCaptureIndicator> indicator = - MediaCaptureDevicesDispatcher::GetInstance()-> - GetMediaStreamCaptureIndicator(); - return indicator->IsBeingMirrored(contents); +bool ShouldTabShowFavicon(int capacity, + bool is_pinned_tab, + bool is_active_tab, + bool has_favicon, + TabMediaState media_state) { + if (!has_favicon) + return false; + int required_capacity = 1; + if (ShouldTabShowCloseButton(capacity, is_pinned_tab, is_active_tab)) + ++required_capacity; + if (ShouldTabShowMediaIndicator( + capacity, is_pinned_tab, is_active_tab, has_favicon, media_state)) { + ++required_capacity; + } + return capacity >= required_capacity; } -bool ShouldShowRecordingIndicator(content::WebContents* contents) { - scoped_refptr<MediaStreamCaptureIndicator> indicator = - MediaCaptureDevicesDispatcher::GetInstance()-> - GetMediaStreamCaptureIndicator(); - // The projecting indicator takes precedence over the recording indicator, but - // if we are projecting and we don't handle the projecting case we want to - // still show the recording indicator. - return indicator->IsCapturingUserMedia(contents) || - indicator->IsBeingMirrored(contents); +bool ShouldTabShowMediaIndicator(int capacity, + bool is_pinned_tab, + bool is_active_tab, + bool has_favicon, + TabMediaState media_state) { + if (media_state == TAB_MEDIA_STATE_NONE) + return false; + const bool audio_playback_active = + (media_state == TAB_MEDIA_STATE_AUDIO_PLAYING); + int required_capacity = (has_favicon && audio_playback_active) ? + 2 : // Must have capacity to also show the favicon. + 1; // Only need capacity to show the capturing/recording indicator. + if (ShouldTabShowCloseButton(capacity, is_pinned_tab, is_active_tab)) + ++required_capacity; + return capacity >= required_capacity; +} + +bool ShouldTabShowCloseButton(int capacity, + bool is_pinned_tab, + bool is_active_tab) { + if (is_pinned_tab) + return false; + else if (is_active_tab) + return true; + else + return capacity >= 3; } bool IsPlayingAudio(content::WebContents* contents) { AudioStreamIndicator* audio_indicator = MediaCaptureDevicesDispatcher::GetInstance()->GetAudioStreamIndicator() .get(); - return audio_indicator->IsPlayingAudio(contents); + return audio_indicator && audio_indicator->IsPlayingAudio(contents); } -bool IsCapturingVideo(content::WebContents* contents) { +TabMediaState GetTabMediaStateForContents(content::WebContents* contents) { + if (!contents) + return TAB_MEDIA_STATE_NONE; + scoped_refptr<MediaStreamCaptureIndicator> indicator = MediaCaptureDevicesDispatcher::GetInstance()-> GetMediaStreamCaptureIndicator(); - return indicator->IsCapturingVideo(contents); + if (indicator) { + if (indicator->IsBeingMirrored(contents)) + return TAB_MEDIA_STATE_CAPTURING; + if (indicator->IsCapturingUserMedia(contents)) + return TAB_MEDIA_STATE_RECORDING; + } + + if (IsPlayingAudio(contents)) + return TAB_MEDIA_STATE_AUDIO_PLAYING; + + return TAB_MEDIA_STATE_NONE; } -bool IsCapturingAudio(content::WebContents* contents) { - scoped_refptr<MediaStreamCaptureIndicator> indicator = - MediaCaptureDevicesDispatcher::GetInstance()-> - GetMediaStreamCaptureIndicator(); - return indicator->IsCapturingAudio(contents); +const gfx::Image& GetTabMediaIndicatorImage(TabMediaState media_state) { + ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); + switch (media_state) { + case TAB_MEDIA_STATE_AUDIO_PLAYING: + return rb.GetNativeImageNamed(IDR_TAB_AUDIO_INDICATOR); + case TAB_MEDIA_STATE_RECORDING: + return rb.GetNativeImageNamed(IDR_TAB_RECORDING_INDICATOR); + case TAB_MEDIA_STATE_CAPTURING: + return rb.GetNativeImageNamed(IDR_TAB_CAPTURE_INDICATOR); + case TAB_MEDIA_STATE_NONE: + break; + } + NOTREACHED(); + return rb.GetNativeImageNamed(IDR_SAD_FAVICON); } -scoped_ptr<gfx::Animation> CreateTabRecordingIndicatorAnimation() { - return TabRecordingIndicatorAnimation::Create().PassAs<gfx::Animation>(); +scoped_ptr<gfx::Animation> CreateTabMediaIndicatorFadeAnimation( + TabMediaState media_state) { + if (media_state == TAB_MEDIA_STATE_RECORDING || + media_state == TAB_MEDIA_STATE_CAPTURING) { + return TabRecordingIndicatorAnimation::Create().PassAs<gfx::Animation>(); + } + + // Note: While it seems silly to use a one-part MultiAnimation, it's the only + // gfx::Animation implementation that lets us control the frame interval. + gfx::MultiAnimation::Parts parts; + const bool is_for_fade_in = (media_state != TAB_MEDIA_STATE_NONE); + parts.push_back(gfx::MultiAnimation::Part( + is_for_fade_in ? kIndicatorFadeInDurationMs : kIndicatorFadeOutDurationMs, + gfx::Tween::EASE_IN)); + const base::TimeDelta interval = + base::TimeDelta::FromMilliseconds(kIndicatorFrameIntervalMs); + scoped_ptr<gfx::MultiAnimation> animation( + new gfx::MultiAnimation(parts, interval)); + animation->set_continuous(false); + return animation.PassAs<gfx::Animation>(); } } // namespace chrome diff --git a/chrome/browser/ui/tabs/tab_utils.h b/chrome/browser/ui/tabs/tab_utils.h index 76b0d37b..114a655 100644 --- a/chrome/browser/ui/tabs/tab_utils.h +++ b/chrome/browser/ui/tabs/tab_utils.h @@ -13,30 +13,66 @@ class WebContents; namespace gfx { class Animation; +class Image; } // namespace gfx -namespace chrome { +// Media state for a tab. In reality, more than one of these may apply. See +// comments for GetTabMediaStateForContents() below. +enum TabMediaState { + TAB_MEDIA_STATE_NONE, + TAB_MEDIA_STATE_RECORDING, // Audio/Video being recorded, consumed by tab. + TAB_MEDIA_STATE_CAPTURING, // Tab contents being captured. + TAB_MEDIA_STATE_AUDIO_PLAYING // Audible audio is playing from the tab. +}; -// Returns whether we should show a projecting favicon indicator for this tab. -bool ShouldShowProjectingIndicator(content::WebContents* contents); +namespace chrome { -// Returns whether we should show a recording favicon indicator for this tab. -bool ShouldShowRecordingIndicator(content::WebContents* contents); +// Logic to determine which components (i.e., close button, favicon, and media +// indicator) of a tab should be shown, given current state. |capacity| +// specifies how many components can be shown, given available tab width. +// +// Precedence rules for deciding what to show when capacity is insufficient to +// show everything: +// +// Active tab: Always show the close button, then the capture/recording +// indicator, then favicon, then audio playback indicator. +// Inactive tab: Capture/Recording indicator, then favicon, then audio +// playback indicator, then close button. +// Pinned tab: Capture/recording indicator, then favicon, then audio +// playback indicator. Never show the close button. +bool ShouldTabShowFavicon(int capacity, + bool is_pinned_tab, + bool is_active_tab, + bool has_favicon, + TabMediaState media_state); +bool ShouldTabShowMediaIndicator(int capacity, + bool is_pinned_tab, + bool is_active_tab, + bool has_favicon, + TabMediaState media_state); +bool ShouldTabShowCloseButton(int capacity, + bool is_pinned_tab, + bool is_active_tab); // Returns whether the given |contents| is playing audio. We might choose to // show an audio favicon indicator for this tab. bool IsPlayingAudio(content::WebContents* contents); -// Returns whether the given |contents| is capturing video. -bool IsCapturingVideo(content::WebContents* contents); +// Returns the media state to be shown by the tab's media indicator. When +// multiple states apply (e.g., tab capture with audio playback), the one most +// relevant to user privacy concerns is selected. +TabMediaState GetTabMediaStateForContents(content::WebContents* contents); -// Returns whether the given |contents| is capturing video. -bool IsCapturingAudio(content::WebContents* contents); +// Returns a cached image, to be shown by the media indicator for the given +// |media_state|. Uses the global ui::ResourceBundle shared instance. +const gfx::Image& GetTabMediaIndicatorImage(TabMediaState media_state); -// Returns an Animation that throbs a few times, and ends in the fully-on -// state. This is meant to be used for the tab recording/capture favicon -// overlay. -scoped_ptr<gfx::Animation> CreateTabRecordingIndicatorAnimation(); +// Returns a non-continuous Animation that performs a fade-in or fade-out +// appropriate for the given |next_media_state|. This is used by the tab media +// indicator to alert the user that recording, tab capture, or audio playback +// has started/stopped. +scoped_ptr<gfx::Animation> CreateTabMediaIndicatorFadeAnimation( + TabMediaState next_media_state); } // namespace chrome diff --git a/chrome/browser/ui/views/tabs/browser_tab_strip_controller.cc b/chrome/browser/ui/views/tabs/browser_tab_strip_controller.cc index e4f195c..1c21c890 100644 --- a/chrome/browser/ui/views/tabs/browser_tab_strip_controller.cc +++ b/chrome/browser/ui/views/tabs/browser_tab_strip_controller.cc @@ -471,17 +471,7 @@ void BrowserTabStripController::SetTabRendererDataFromModel( data->mini = model_->IsMiniTab(model_index); data->blocked = model_->IsTabBlocked(model_index); data->app = extensions::TabHelper::FromWebContents(contents)->is_app(); - if (chrome::ShouldShowProjectingIndicator(contents)) - data->capture_state = TabRendererData::CAPTURE_STATE_PROJECTING; - else if (chrome::ShouldShowRecordingIndicator(contents)) - data->capture_state = TabRendererData::CAPTURE_STATE_RECORDING; - else - data->capture_state = TabRendererData::CAPTURE_STATE_NONE; - - if (chrome::IsPlayingAudio(contents)) - data->audio_state = TabRendererData::AUDIO_STATE_PLAYING; - else - data->audio_state = TabRendererData::AUDIO_STATE_NONE; + data->media_state = chrome::GetTabMediaStateForContents(contents); } void BrowserTabStripController::SetTabDataAt(content::WebContents* web_contents, diff --git a/chrome/browser/ui/views/tabs/tab.cc b/chrome/browser/ui/views/tabs/tab.cc index be34f25..6990ce3 100644 --- a/chrome/browser/ui/views/tabs/tab.cc +++ b/chrome/browser/ui/views/tabs/tab.cc @@ -242,12 +242,6 @@ const double kImmersiveTabMinThrobOpacity = 0.66; // Number of steps in the immersive mode loading animation. const int kImmersiveLoadingStepCount = 32; -// Scale to resize the current favicon by when projecting. -const double kProjectingFaviconResizeScale = 0.75; - -// Scale to resize the projection sheet glow by. -const double kProjectingGlowResizeScale = 2.0; - void DrawIconAtLocation(gfx::Canvas* canvas, const gfx::ImageSkia& image, int image_offset, @@ -458,11 +452,12 @@ Tab::Tab(TabController* controller) loading_animation_frame_(0), immersive_loading_step_(0), should_display_crashed_favicon_(false), + animating_media_state_(TAB_MEDIA_STATE_NONE), theme_provider_(NULL), tab_activated_with_last_gesture_begin_(false), hover_controller_(this), showing_icon_(false), - showing_audio_indicator_(false), + showing_media_indicator_(false), showing_close_button_(false), close_button_color_(0) { InitTabResources(); @@ -517,11 +512,7 @@ void Tab::SetData(const TabRendererData& data) { if (data_.IsCrashed()) { if (!should_display_crashed_favicon_ && !IsPerformingCrashAnimation()) { - // Crash animation overrides the other icon animations. - old.audio_state = TabRendererData::AUDIO_STATE_NONE; - data_.audio_state = TabRendererData::AUDIO_STATE_NONE; - data_.capture_state = TabRendererData::CAPTURE_STATE_NONE; - old.capture_state = TabRendererData::CAPTURE_STATE_NONE; + data_.media_state = TAB_MEDIA_STATE_NONE; #if defined(OS_CHROMEOS) // On Chrome OS, we reload killed tabs automatically when the user // switches to them. Don't display animations for these unless they're @@ -534,16 +525,18 @@ void Tab::SetData(const TabRendererData& data) { StartCrashAnimation(); #endif } - } else if (!data_.CaptureActive() && old.CaptureActive()) { - StopIconAnimation(); - } else if (data_.CaptureActive() && !old.CaptureActive()) { - StartRecordingAnimation(); } else { if (IsPerformingCrashAnimation()) - StopIconAnimation(); + StopCrashAnimation(); ResetCrashedFavicon(); } + if (data_.media_state != old.media_state) { + if (data_.media_state != TAB_MEDIA_STATE_NONE) + animating_media_state_ = data_.media_state; + StartMediaIndicatorAnimation(); + } + if (old.mini != data_.mini) { if (tab_animation_.get() && tab_animation_->is_animating()) { tab_animation_->Stop(); @@ -683,10 +676,14 @@ void Tab::AnimationProgressed(const gfx::Animation* animation) { } void Tab::AnimationCanceled(const gfx::Animation* animation) { + if (media_indicator_animation_ == animation) + animating_media_state_ = data_.media_state; SchedulePaint(); } void Tab::AnimationEnded(const gfx::Animation* animation) { + if (media_indicator_animation_ == animation) + animating_media_state_ = data_.media_state; SchedulePaint(); } @@ -801,21 +798,22 @@ void Tab::Layout() { close_button_->SetVisible(false); } - showing_audio_indicator_ = ShouldShowAudioIndicator(); - if (showing_audio_indicator_) { - ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); - gfx::ImageSkia audio_indicator_image( - *rb.GetImageSkiaNamed(IDR_TAB_AUDIO_INDICATOR)); - const int top = - top_padding() + (content_height - audio_indicator_image.height()) / 2; + showing_media_indicator_ = ShouldShowMediaIndicator(); + if (showing_media_indicator_) { + const gfx::Image& media_indicator_image = + chrome::GetTabMediaIndicatorImage(animating_media_state_); + media_indicator_bounds_.set_width(media_indicator_image.Width()); + media_indicator_bounds_.set_height(media_indicator_image.Height()); + media_indicator_bounds_.set_y( + top_padding() + + (content_height - media_indicator_bounds_.height()) / 2); const int right = showing_close_button_ ? close_button_->x() + close_button_->GetInsets().left() : lb.right(); - const int left = std::max(lb.x(), right - audio_indicator_image.width()); - audio_indicator_bounds_.SetRect(left, top, - tab_icon_size(), tab_icon_size()); - MaybeAdjustLeftForMiniTab(&audio_indicator_bounds_); + media_indicator_bounds_.set_x( + std::max(lb.x(), right - media_indicator_bounds_.width())); + MaybeAdjustLeftForMiniTab(&media_indicator_bounds_); } else { - audio_indicator_bounds_.SetRect(lb.x(), lb.y(), 0, 0); + media_indicator_bounds_.SetRect(lb.x(), lb.y(), 0, 0); } const int title_text_offset = is_host_desktop_type_ash ? @@ -834,8 +832,8 @@ void Tab::Layout() { title_top -= (text_height - minimum_size.height()) / 2; int title_width; - if (showing_audio_indicator_) { - title_width = audio_indicator_bounds_.x() - kTitleCloseButtonSpacing - + if (showing_media_indicator_) { + title_width = media_indicator_bounds_.x() - kTitleCloseButtonSpacing - title_left; } else if (close_button_->visible()) { // The close button has an empty border with some padding (see details @@ -1091,10 +1089,10 @@ void Tab::DataChanged(const TabRendererData& old) { void Tab::PaintTab(gfx::Canvas* canvas) { // See if the model changes whether the icons should be painted. const bool show_icon = ShouldShowIcon(); - const bool show_audio_indicator = ShouldShowAudioIndicator(); + const bool show_media_indicator = ShouldShowMediaIndicator(); const bool show_close_button = ShouldShowCloseBox(); if (show_icon != showing_icon_ || - show_audio_indicator != showing_audio_indicator_ || + show_media_indicator != showing_media_indicator_ || show_close_button != showing_close_button_) { Layout(); } @@ -1112,8 +1110,8 @@ void Tab::PaintTab(gfx::Canvas* canvas) { if (show_icon) PaintIcon(canvas); - if (show_audio_indicator) - PaintAudioIndicator(canvas); + if (show_media_indicator) + PaintMediaIndicator(canvas); // If the close button color has changed, generate a new one. if (!close_button_color_ || title_color != close_button_color_) { @@ -1410,8 +1408,8 @@ void Tab::PaintIcon(gfx::Canvas* canvas) { bounds.set_x(GetMirroredXForRect(bounds)); - // Paint network activity (aka throbber) animation frame. if (data().network_state != TabRendererData::NETWORK_STATE_NONE) { + // Paint network activity (aka throbber) animation frame. ui::ThemeProvider* tp = GetThemeProvider(); gfx::ImageSkia frames(*tp->GetImageSkiaNamed( (data().network_state == TabRendererData::NETWORK_STATE_WAITING) ? @@ -1422,13 +1420,8 @@ void Tab::PaintIcon(gfx::Canvas* canvas) { DrawIconCenter(canvas, frames, image_offset, icon_size, icon_size, bounds, false, SkPaint()); - return; - } - - // Paint regular icon and potentially overlays. - canvas->Save(); - canvas->ClipRect(GetLocalBounds()); - if (should_display_crashed_favicon_) { + } else if (should_display_crashed_favicon_) { + // Paint crash favicon. ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); gfx::ImageSkia crashed_favicon(*rb.GetImageSkiaNamed(IDR_SAD_FAVICON)); bounds.set_y(bounds.y() + favicon_hiding_offset_); @@ -1436,158 +1429,35 @@ void Tab::PaintIcon(gfx::Canvas* canvas) { crashed_favicon.width(), crashed_favicon.height(), bounds, true, SkPaint()); - } else { - if (!data().favicon.isNull()) { - if (data().capture_state == TabRendererData::CAPTURE_STATE_PROJECTING) { - // If projecting, shrink favicon and add projection screen instead. - gfx::ImageSkia resized_icon = - gfx::ImageSkiaOperations::CreateResizedImage( - data().favicon, - skia::ImageOperations::RESIZE_BEST, - gfx::Size(data().favicon.width() * - kProjectingFaviconResizeScale, - data().favicon.height() * - kProjectingFaviconResizeScale)); - - gfx::Rect resized_bounds(bounds); - // Need to shift it up a bit vertically because the projection screen - // is thinner on the top and bottom. - resized_bounds.set_y(resized_bounds.y() - 1); - - DrawIconCenter(canvas, resized_icon, 0, - resized_icon.width(), - resized_icon.height(), - resized_bounds, true, SkPaint()); - - ui::ThemeProvider* tp = GetThemeProvider(); - gfx::ImageSkia projection_screen( - *tp->GetImageSkiaNamed(IDR_TAB_CAPTURE)); - DrawIconCenter(canvas, projection_screen, 0, - data().favicon.width(), - data().favicon.height(), - bounds, true, SkPaint()); - } else { - DrawIconCenter(canvas, data().favicon, 0, - data().favicon.width(), - data().favicon.height(), - bounds, true, SkPaint()); - } - } + } else if (!data().favicon.isNull()) { + // Paint the normal favicon. + DrawIconCenter(canvas, data().favicon, 0, + data().favicon.width(), + data().favicon.height(), + bounds, true, SkPaint()); } - canvas->Restore(); - - // Paint recording or projecting animation overlay. - if (data().capture_state != TabRendererData::CAPTURE_STATE_NONE) - PaintCaptureState(canvas, bounds); } -void Tab::PaintCaptureState(gfx::Canvas* canvas, gfx::Rect bounds) { - SkPaint paint; - paint.setAntiAlias(true); - DCHECK(capture_icon_animation_.get()); - paint.setAlpha(capture_icon_animation_->GetCurrentValue() * SK_AlphaOPAQUE); - ui::ThemeProvider* tp = GetThemeProvider(); - - if (data().capture_state == TabRendererData::CAPTURE_STATE_PROJECTING) { - // If projecting, add projection glow animation. - gfx::Rect glow_bounds(bounds); - glow_bounds.set_x(glow_bounds.x() - (32 - 24)); - glow_bounds.set_y(0); - glow_bounds.set_width(glow_bounds.width() * kProjectingGlowResizeScale); - glow_bounds.set_height(glow_bounds.height() * kProjectingGlowResizeScale); - - gfx::ImageSkia projection_glow( - *tp->GetImageSkiaNamed(IDR_TAB_CAPTURE_GLOW)); - DrawIconCenter(canvas, projection_glow, 0, projection_glow.width(), - projection_glow.height(), glow_bounds, false, paint); - } else if (data().capture_state == TabRendererData::CAPTURE_STATE_RECORDING) { - // If recording, fade the recording icon on top of the favicon with a mask - // around/behind it. - gfx::ImageSkia recording_dot(*tp->GetImageSkiaNamed(IDR_TAB_RECORDING)); - gfx::ImageSkia recording_dot_mask( - *tp->GetImageSkiaNamed(IDR_TAB_RECORDING_MASK)); - gfx::ImageSkia tab_background; - if (IsActive()) { - tab_background = *tp->GetImageSkiaNamed(IDR_THEME_TOOLBAR); - } else { - int tab_id; - int frame_id_dummy; - views::Widget* widget = GetWidget(); - GetTabIdAndFrameId(widget, &tab_id, &frame_id_dummy); - tab_background = *tp->GetImageSkiaNamed(tab_id); - } - - // This is the offset from the favicon bottom right corner for the mask, - // given that the recording dot is drawn in the bottom right corner. - int mask_offset_from_right = recording_dot.width() + - (recording_dot_mask.width() - recording_dot.width()) / 2; - int mask_offset_from_bottom = recording_dot.height() + - (recording_dot_mask.height() - recording_dot.height()) / 2; - - int mask_dst_x = bounds.x() + bounds.width() - mask_offset_from_right; - int mask_dst_y = bounds.y() + bounds.height() - mask_offset_from_bottom; - - // Crop the background image at the correct position and create a mask - // from the background. - int offset_x = GetMirroredX() + background_offset_.x() + mask_dst_x; - int offset_y = mask_dst_y; - gfx::ImageSkia tab_background_cropped = - gfx::ImageSkiaOperations::CreateTiledImage( - tab_background, offset_x, offset_y, - recording_dot_mask.width(), recording_dot_mask.height()); - gfx::ImageSkia recording_dot_mask_with_bg = - gfx::ImageSkiaOperations::CreateMaskedImage(tab_background_cropped, - recording_dot_mask); - - // Draw the mask. - DrawIconAtLocation(canvas, recording_dot_mask_with_bg, 0, - mask_dst_x, mask_dst_y, - recording_dot_mask.width(), - recording_dot_mask.height(), - false, SkPaint()); - - // Potentially draw an alpha of the active bg image. - double throb_value = GetThrobValue(); - if (!IsActive() && throb_value > 0) { - tab_background = *tp->GetImageSkiaNamed(IDR_THEME_TOOLBAR); - tab_background_cropped = gfx::ImageSkiaOperations::CreateTiledImage( - tab_background, offset_x, offset_y, - recording_dot_mask.width(), recording_dot_mask.height()); - recording_dot_mask_with_bg = gfx::ImageSkiaOperations::CreateMaskedImage( - tab_background_cropped, recording_dot_mask); - - canvas->SaveLayerAlpha(static_cast<int>(throb_value * 0xff), - GetLocalBounds()); - DrawIconAtLocation(canvas, recording_dot_mask_with_bg, 0, - mask_dst_x, mask_dst_y, - recording_dot_mask.width(), - recording_dot_mask.height(), - false, SkPaint()); - canvas->Restore(); - } - - // Draw the recording icon. - DrawIconBottomRight(canvas, recording_dot, 0, - recording_dot.width(), recording_dot.height(), - bounds, false, paint); - } else { - NOTREACHED(); - } -} - -void Tab::PaintAudioIndicator(gfx::Canvas* canvas) { - if (audio_indicator_bounds_.IsEmpty()) +void Tab::PaintMediaIndicator(gfx::Canvas* canvas) { + if (media_indicator_bounds_.IsEmpty() || !media_indicator_animation_) return; - gfx::Rect bounds = audio_indicator_bounds_; + gfx::Rect bounds = media_indicator_bounds_; bounds.set_x(GetMirroredXForRect(bounds)); - ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); - const gfx::ImageSkia audio_indicator_image( - *rb.GetImageSkiaNamed(IDR_TAB_AUDIO_INDICATOR)); - DrawIconAtLocation(canvas, audio_indicator_image, 0, - bounds.x(), bounds.y(), audio_indicator_image.width(), - audio_indicator_image.height(), true, SkPaint()); + SkPaint paint; + paint.setAntiAlias(true); + double opaqueness = media_indicator_animation_->GetCurrentValue(); + if (data_.media_state == TAB_MEDIA_STATE_NONE) + opaqueness = 1.0 - opaqueness; // Fading out, not in. + paint.setAlpha(opaqueness * SK_AlphaOPAQUE); + + const gfx::ImageSkia& media_indicator_image = + *(chrome::GetTabMediaIndicatorImage(animating_media_state_). + ToImageSkia()); + DrawIconAtLocation(canvas, media_indicator_image, 0, + bounds.x(), bounds.y(), media_indicator_image.width(), + media_indicator_image.height(), true, paint); } void Tab::PaintTitle(gfx::Canvas* canvas, SkColor title_color) { @@ -1669,50 +1539,32 @@ void Tab::AdvanceLoadingAnimation(TabRendererData::NetworkState old_state, int Tab::IconCapacity() const { if (height() < GetMinimumUnselectedSize().height()) return 0; + const int available_width = + std::max(0, width() - left_padding() - right_padding()); + const int width_per_icon = tab_icon_size(); const int kPaddingBetweenIcons = 2; - return (width() - left_padding() - right_padding()) / - (tab_icon_size() + kPaddingBetweenIcons); + if (available_width >= width_per_icon && + available_width < (width_per_icon + kPaddingBetweenIcons)) { + return 1; + } + return available_width / (width_per_icon + kPaddingBetweenIcons); } bool Tab::ShouldShowIcon() const { - if (!data().show_icon) - return false; - const bool should_show_audio_indicator = ShouldShowAudioIndicator(); - if (data().mini && height() >= GetMinimumUnselectedSize().height()) { - // Audio indicator always takes precendence over the favicon for mini tabs. - return !should_show_audio_indicator; - } - int required_capacity = should_show_audio_indicator ? 2 : 1; - if (IsActive()) { - // Active tabs give priority to the close button, then the audio indicator, - // then the favicon. - ++required_capacity; - } else { - // Non-active tabs give priority to the audio indicator, then the favicon, - // and finally the close button. - } - return IconCapacity() >= required_capacity; + return chrome::ShouldTabShowFavicon( + IconCapacity(), data().mini, IsActive(), data().show_icon, + animating_media_state_); } -bool Tab::ShouldShowAudioIndicator() const { - // Note: If the capture indicator is active, then do not show the audio - // indicator. This allows the favicon "throbber" animation to be shown in - // small-width situations. - if (!data().AudioActive() || data().CaptureActive()) - return false; - if (data().mini && height() >= GetMinimumUnselectedSize().height()) - return true; - if (IsActive()) { - // The active tab clips the audio indicator before the close button. - return IconCapacity() >= 2; - } - // Non-active tabs clip close button before the audio indicator. - return IconCapacity() >= 1; +bool Tab::ShouldShowMediaIndicator() const { + return chrome::ShouldTabShowMediaIndicator( + IconCapacity(), data().mini, IsActive(), data().show_icon, + animating_media_state_); } bool Tab::ShouldShowCloseBox() const { - // The active tab never clips close button. - return !data().mini && (IsActive() || IconCapacity() >= 3); + return chrome::ShouldTabShowCloseButton( + IconCapacity(), data().mini, IsActive()); } double Tab::GetThrobValue() { @@ -1746,28 +1598,26 @@ void Tab::ResetCrashedFavicon() { should_display_crashed_favicon_ = false; } -void Tab::StopIconAnimation() { +void Tab::StopCrashAnimation() { crash_icon_animation_.reset(); - capture_icon_animation_.reset(); } void Tab::StartCrashAnimation() { - capture_icon_animation_.reset(); crash_icon_animation_.reset(new FaviconCrashAnimation(this)); crash_icon_animation_->Start(); } -void Tab::StartRecordingAnimation() { - crash_icon_animation_.reset(); - capture_icon_animation_ = chrome::CreateTabRecordingIndicatorAnimation(); - capture_icon_animation_->set_delegate(this); - capture_icon_animation_->Start(); -} - bool Tab::IsPerformingCrashAnimation() const { return crash_icon_animation_.get() && data_.IsCrashed(); } +void Tab::StartMediaIndicatorAnimation() { + media_indicator_animation_ = + chrome::CreateTabMediaIndicatorFadeAnimation(data_.media_state); + media_indicator_animation_->set_delegate(this); + media_indicator_animation_->Start(); +} + void Tab::ScheduleIconPaint() { gfx::Rect bounds = GetIconBounds(); if (bounds.IsEmpty()) diff --git a/chrome/browser/ui/views/tabs/tab.h b/chrome/browser/ui/views/tabs/tab.h index 4ef7567..1deebf1 100644 --- a/chrome/browser/ui/views/tabs/tab.h +++ b/chrome/browser/ui/views/tabs/tab.h @@ -193,7 +193,7 @@ class Tab : public gfx::AnimationDelegate, const gfx::Rect& GetTitleBounds() const; const gfx::Rect& GetIconBounds() const; - // Invoked from Layout to adjust the position of the favicon or audio + // Invoked from Layout to adjust the position of the favicon or media // indicator for mini tabs. void MaybeAdjustLeftForMiniTab(gfx::Rect* bounds) const; @@ -216,10 +216,9 @@ class Tab : public gfx::AnimationDelegate, int tab_id); void PaintActiveTabBackground(gfx::Canvas* canvas); - // Paints the icon, audio indicator icon, etc., mirrored for RTL if needed. + // Paints the favicon, media indicator icon, etc., mirrored for RTL if needed. void PaintIcon(gfx::Canvas* canvas); - void PaintCaptureState(gfx::Canvas* canvas, gfx::Rect bounds); - void PaintAudioIndicator(gfx::Canvas* canvas); + void PaintMediaIndicator(gfx::Canvas* canvas); void PaintTitle(gfx::Canvas* canvas, SkColor title_color); // Invoked if data_.network_state changes, or the network_state is not none. @@ -233,8 +232,8 @@ class Tab : public gfx::AnimationDelegate, // Returns whether the Tab should display a favicon. bool ShouldShowIcon() const; - // Returns whether the Tab should display the audio indicator. - bool ShouldShowAudioIndicator() const; + // Returns whether the Tab should display the media indicator. + bool ShouldShowMediaIndicator() const; // Returns whether the Tab should display a close button. bool ShouldShowCloseBox() const; @@ -251,13 +250,16 @@ class Tab : public gfx::AnimationDelegate, void DisplayCrashedFavicon(); void ResetCrashedFavicon(); - void StopIconAnimation(); + void StopCrashAnimation(); void StartCrashAnimation(); - void StartRecordingAnimation(); // Returns true if the crash animation is currently running. bool IsPerformingCrashAnimation() const; + // Starts the media indicator fade-in/out animation. There's no stop method + // because this is not a continuous animation. + void StartMediaIndicatorAnimation(); + // Schedules repaint task for icon. void ScheduleIconPaint(); @@ -315,13 +317,17 @@ class Tab : public gfx::AnimationDelegate, bool should_display_crashed_favicon_; - // The tab and the icon can both be animating. The tab 'throbs' by changing - // color. The icon can have one of several of animations like crashing, - // recording, projecting, etc. Note that the icon animation related to network - // state does not have an animation associated with it. + // Whole-tab throbbing "pulse" animation. scoped_ptr<gfx::Animation> tab_animation_; + + // Crash icon animation (in place of favicon). scoped_ptr<gfx::LinearAnimation> crash_icon_animation_; - scoped_ptr<gfx::Animation> capture_icon_animation_; + + // Media indicator fade-in/out animation (i.e., only on show/hide, not a + // continuous animation). + scoped_ptr<gfx::Animation> media_indicator_animation_; + TabMediaState animating_media_state_; + scoped_refptr<gfx::AnimationContainer> animation_container_; views::ImageButton* close_button_; @@ -335,7 +341,7 @@ class Tab : public gfx::AnimationDelegate, // The bounds of various sections of the display. gfx::Rect favicon_bounds_; gfx::Rect title_bounds_; - gfx::Rect audio_indicator_bounds_; + gfx::Rect media_indicator_bounds_; // The offset used to paint the inactive background image. gfx::Point background_offset_; @@ -355,9 +361,9 @@ class Tab : public gfx::AnimationDelegate, // changes and layout appropriately. bool showing_icon_; - // Whether we're showing the audio indicator. It is cached so that we can + // Whether we're showing the media indicator. It is cached so that we can // detect when it changes and layout appropriately. - bool showing_audio_indicator_; + bool showing_media_indicator_; // Whether we are showing the close button. It is cached so that we can // detect when it changes and layout appropriately. diff --git a/chrome/browser/ui/views/tabs/tab_renderer_data.cc b/chrome/browser/ui/views/tabs/tab_renderer_data.cc index 9393cf5..6057b32 100644 --- a/chrome/browser/ui/views/tabs/tab_renderer_data.cc +++ b/chrome/browser/ui/views/tabs/tab_renderer_data.cc @@ -13,8 +13,7 @@ TabRendererData::TabRendererData() mini(false), blocked(false), app(false), - capture_state(CAPTURE_STATE_NONE), - audio_state(AUDIO_STATE_NONE) { + media_state(TAB_MEDIA_STATE_NONE) { } TabRendererData::~TabRendererData() {} @@ -32,6 +31,5 @@ bool TabRendererData::Equals(const TabRendererData& data) { mini == data.mini && blocked == data.blocked && app == data.app && - capture_state == data.capture_state && - audio_state == data.audio_state; + media_state == data.media_state; } diff --git a/chrome/browser/ui/views/tabs/tab_renderer_data.h b/chrome/browser/ui/views/tabs/tab_renderer_data.h index b06fea8..90c8000 100644 --- a/chrome/browser/ui/views/tabs/tab_renderer_data.h +++ b/chrome/browser/ui/views/tabs/tab_renderer_data.h @@ -7,6 +7,7 @@ #include "base/process/kill.h" #include "base/strings/string16.h" +#include "chrome/browser/ui/tabs/tab_utils.h" #include "chrome/browser/ui/views/chrome_views_export.h" #include "ui/gfx/image/image_skia.h" #include "url/gurl.h" @@ -22,21 +23,6 @@ struct CHROME_VIEWS_EXPORT TabRendererData { NETWORK_STATE_LOADING, // connected, transferring data. }; - // Capture state of this tab. If a WebRTC media stream is active, then it is - // recording. If tab capturing is active then it is projecting. - enum CaptureState { - CAPTURE_STATE_NONE, - CAPTURE_STATE_RECORDING, - CAPTURE_STATE_PROJECTING - }; - - // Audio playing state of this tab. If muting is added this is where it - // should go. - enum AudioState { - AUDIO_STATE_NONE, - AUDIO_STATE_PLAYING - }; - TabRendererData(); ~TabRendererData(); @@ -49,14 +35,6 @@ struct CHROME_VIEWS_EXPORT TabRendererData { crashed_status == base::TERMINATION_STATUS_ABNORMAL_TERMINATION); } - bool AudioActive() const { - return audio_state != AUDIO_STATE_NONE; - } - - bool CaptureActive() const { - return capture_state != CAPTURE_STATE_NONE; - } - // Returns true if the TabRendererData is same as given |data|. bool Equals(const TabRendererData& data); @@ -71,8 +49,7 @@ struct CHROME_VIEWS_EXPORT TabRendererData { bool mini; bool blocked; bool app; - CaptureState capture_state; - AudioState audio_state; + TabMediaState media_state; }; #endif // CHROME_BROWSER_UI_VIEWS_TABS_TAB_RENDERER_DATA_H_ diff --git a/chrome/browser/ui/views/tabs/tab_unittest.cc b/chrome/browser/ui/views/tabs/tab_unittest.cc index 16ab690..c16a508 100644 --- a/chrome/browser/ui/views/tabs/tab_unittest.cc +++ b/chrome/browser/ui/views/tabs/tab_unittest.cc @@ -73,18 +73,24 @@ class TabTest : public views::ViewsTestBase { TabTest() {} virtual ~TabTest() {} - static bool IconAnimationInvariant(const Tab& tab) { - return tab.data().CaptureActive() == !!tab.capture_icon_animation_.get(); + static void DisableMediaIndicatorAnimation(Tab* tab) { + tab->media_indicator_animation_.reset(); + tab->animating_media_state_ = tab->data_.media_state; } static void CheckForExpectedLayoutAndVisibilityOfElements(const Tab& tab) { // Check whether elements are visible when they are supposed to be, given // Tab size and TabRendererData state. if (tab.data_.mini) { - if (tab.data_.CaptureActive()) + EXPECT_EQ(1, tab.IconCapacity()); + if (tab.data_.media_state == TAB_MEDIA_STATE_CAPTURING || + tab.data_.media_state == TAB_MEDIA_STATE_RECORDING) { + EXPECT_FALSE(tab.ShouldShowIcon()); + EXPECT_TRUE(tab.ShouldShowMediaIndicator()); + } else { EXPECT_TRUE(tab.ShouldShowIcon()); - else - EXPECT_TRUE(tab.ShouldShowIcon() != tab.ShouldShowAudioIndicator()); + EXPECT_FALSE(tab.ShouldShowMediaIndicator()); + } EXPECT_FALSE(tab.ShouldShowCloseBox()); } else if (tab.IsActive()) { EXPECT_TRUE(tab.ShouldShowCloseBox()); @@ -92,23 +98,25 @@ class TabTest : public views::ViewsTestBase { case 0: case 1: EXPECT_FALSE(tab.ShouldShowIcon()); - EXPECT_FALSE(tab.ShouldShowAudioIndicator()); + EXPECT_FALSE(tab.ShouldShowMediaIndicator()); break; case 2: - if (tab.data_.CaptureActive()) + if (tab.data_.media_state == TAB_MEDIA_STATE_CAPTURING || + tab.data_.media_state == TAB_MEDIA_STATE_RECORDING) { + EXPECT_FALSE(tab.ShouldShowIcon()); + EXPECT_TRUE(tab.ShouldShowMediaIndicator()); + } else { EXPECT_TRUE(tab.ShouldShowIcon()); - else - EXPECT_TRUE(tab.ShouldShowIcon() != tab.ShouldShowAudioIndicator()); + EXPECT_FALSE(tab.ShouldShowMediaIndicator()); + } break; default: EXPECT_LE(3, tab.IconCapacity()); EXPECT_TRUE(tab.ShouldShowIcon()); - if (tab.data_.CaptureActive()) { - EXPECT_FALSE(tab.ShouldShowAudioIndicator()); - } else { - EXPECT_TRUE(tab.data_.AudioActive() == - tab.ShouldShowAudioIndicator()); - } + if (tab.data_.media_state != TAB_MEDIA_STATE_NONE) + EXPECT_TRUE(tab.ShouldShowMediaIndicator()); + else + EXPECT_FALSE(tab.ShouldShowMediaIndicator()); break; } } else { // Tab not active and not mini tab. @@ -116,24 +124,26 @@ class TabTest : public views::ViewsTestBase { case 0: EXPECT_FALSE(tab.ShouldShowCloseBox()); EXPECT_FALSE(tab.ShouldShowIcon()); - EXPECT_FALSE(tab.ShouldShowAudioIndicator()); + EXPECT_FALSE(tab.ShouldShowMediaIndicator()); break; case 1: EXPECT_FALSE(tab.ShouldShowCloseBox()); - if (tab.data_.CaptureActive()) + if (tab.data_.media_state == TAB_MEDIA_STATE_CAPTURING || + tab.data_.media_state == TAB_MEDIA_STATE_RECORDING) { + EXPECT_FALSE(tab.ShouldShowIcon()); + EXPECT_TRUE(tab.ShouldShowMediaIndicator()); + } else { EXPECT_TRUE(tab.ShouldShowIcon()); - else - EXPECT_TRUE(tab.ShouldShowIcon() != tab.ShouldShowAudioIndicator()); + EXPECT_FALSE(tab.ShouldShowMediaIndicator()); + } break; default: EXPECT_LE(2, tab.IconCapacity()); EXPECT_TRUE(tab.ShouldShowIcon()); - if (tab.data_.CaptureActive()) { - EXPECT_FALSE(tab.ShouldShowAudioIndicator()); - } else { - EXPECT_TRUE(tab.data_.AudioActive() == - tab.ShouldShowAudioIndicator()); - } + if (tab.data_.media_state != TAB_MEDIA_STATE_NONE) + EXPECT_TRUE(tab.ShouldShowMediaIndicator()); + else + EXPECT_FALSE(tab.ShouldShowMediaIndicator()); break; } } @@ -148,19 +158,19 @@ class TabTest : public views::ViewsTestBase { EXPECT_LE(contents_bounds.y(), tab.favicon_bounds_.y()); EXPECT_LE(tab.favicon_bounds_.bottom(), contents_bounds.bottom()); } - if (tab.ShouldShowIcon() && tab.ShouldShowAudioIndicator()) - EXPECT_LE(tab.favicon_bounds_.right(), tab.audio_indicator_bounds_.x()); - if (tab.ShouldShowAudioIndicator()) { + if (tab.ShouldShowIcon() && tab.ShouldShowMediaIndicator()) + EXPECT_LE(tab.favicon_bounds_.right(), tab.media_indicator_bounds_.x()); + if (tab.ShouldShowMediaIndicator()) { if (tab.title_bounds_.width() > 0) - EXPECT_LE(tab.title_bounds_.right(), tab.audio_indicator_bounds_.x()); - EXPECT_LE(tab.audio_indicator_bounds_.right(), contents_bounds.right()); - EXPECT_LE(contents_bounds.y(), tab.audio_indicator_bounds_.y()); - EXPECT_LE(tab.audio_indicator_bounds_.bottom(), contents_bounds.bottom()); + EXPECT_LE(tab.title_bounds_.right(), tab.media_indicator_bounds_.x()); + EXPECT_LE(tab.media_indicator_bounds_.right(), contents_bounds.right()); + EXPECT_LE(contents_bounds.y(), tab.media_indicator_bounds_.y()); + EXPECT_LE(tab.media_indicator_bounds_.bottom(), contents_bounds.bottom()); } - if (tab.ShouldShowAudioIndicator() && tab.ShouldShowCloseBox()) { - // Note: The audio indicator can overlap the left-insets of the close box, + if (tab.ShouldShowMediaIndicator() && tab.ShouldShowCloseBox()) { + // Note: The media indicator can overlap the left-insets of the close box, // but should otherwise be to the left of the close button. - EXPECT_LE(tab.audio_indicator_bounds_.right(), + EXPECT_LE(tab.media_indicator_bounds_.right(), tab.close_button_->bounds().x() + tab.close_button_->GetInsets().left()); } @@ -210,6 +220,11 @@ TEST_F(TabTest, HitTestTopPixel) { } TEST_F(TabTest, LayoutAndVisibilityOfElements) { + static const TabMediaState kMediaStatesToTest[] = { + TAB_MEDIA_STATE_NONE, TAB_MEDIA_STATE_CAPTURING, + TAB_MEDIA_STATE_AUDIO_PLAYING + }; + FakeTabController controller; Tab tab(&controller); @@ -223,41 +238,41 @@ TEST_F(TabTest, LayoutAndVisibilityOfElements) { // results. for (int is_mini_tab = 0; is_mini_tab < 2; ++is_mini_tab) { for (int is_active_tab = 0; is_active_tab < 2; ++is_active_tab) { - for (int audio_state = TabRendererData::AUDIO_STATE_NONE; - audio_state <= TabRendererData::AUDIO_STATE_PLAYING; ++audio_state) { - for (int capture_state = TabRendererData::CAPTURE_STATE_NONE; - capture_state <= TabRendererData::CAPTURE_STATE_PROJECTING; - ++capture_state) { - SCOPED_TRACE(::testing::Message() - << (is_active_tab ? "Active" : "Inactive") << ' ' - << (is_mini_tab ? "Mini " : "") - << "Tab with audio_state=" << audio_state - << " and capture_state=" << capture_state); - data.mini = !!is_mini_tab; - controller.set_active_tab(!!is_active_tab); - data.audio_state = - static_cast<TabRendererData::AudioState>(audio_state); - data.capture_state = - static_cast<TabRendererData::CaptureState>(capture_state); - tab.SetData(data); - - // Test layout for every width from standard to minimum. - gfx::Rect bounds(gfx::Point(0, 0), Tab::GetStandardSize()); - int min_width; - if (is_mini_tab) { - bounds.set_width(Tab::GetMiniWidth()); - min_width = Tab::GetMiniWidth(); - } else { - min_width = is_active_tab ? Tab::GetMinimumSelectedSize().width() : - Tab::GetMinimumUnselectedSize().width(); - } - while (bounds.width() >= min_width) { - SCOPED_TRACE(::testing::Message() - << "bounds=" << bounds.ToString()); - tab.SetBoundsRect(bounds); // Invokes Tab::Layout(). - CheckForExpectedLayoutAndVisibilityOfElements(tab); - bounds.set_width(bounds.width() - 1); - } + for (size_t media_state_index = 0; + media_state_index < arraysize(kMediaStatesToTest); + ++media_state_index) { + const TabMediaState media_state = kMediaStatesToTest[media_state_index]; + SCOPED_TRACE(::testing::Message() + << (is_active_tab ? "Active" : "Inactive") << ' ' + << (is_mini_tab ? "Mini " : "") + << "Tab with media indicator state " << media_state); + + data.mini = !!is_mini_tab; + controller.set_active_tab(!!is_active_tab); + data.media_state = media_state; + tab.SetData(data); + + // Disable the media indicator animation so that the layout/visibility + // logic can be tested effectively. If the animation was left enabled, + // the ShouldShowMediaIndicator() method would return true during + // fade-out transitions. + DisableMediaIndicatorAnimation(&tab); + + // Test layout for every width from standard to minimum. + gfx::Rect bounds(gfx::Point(0, 0), Tab::GetStandardSize()); + int min_width; + if (is_mini_tab) { + bounds.set_width(Tab::GetMiniWidth()); + min_width = Tab::GetMiniWidth(); + } else { + min_width = is_active_tab ? Tab::GetMinimumSelectedSize().width() : + Tab::GetMinimumUnselectedSize().width(); + } + while (bounds.width() >= min_width) { + SCOPED_TRACE(::testing::Message() << "bounds=" << bounds.ToString()); + tab.SetBoundsRect(bounds); // Invokes Tab::Layout(). + CheckForExpectedLayoutAndVisibilityOfElements(tab); + bounds.set_width(bounds.width() - 1); } } } @@ -282,59 +297,3 @@ TEST_F(TabTest, CloseButtonLayout) { // Also make sure the close button is sized as large as the tab. EXPECT_EQ(50, tab.close_button_->bounds().height()); } - -TEST_F(TabTest, RecordingAndProjectingActivityIndicators) { - FakeTabController controller; - Tab tab(&controller); - tab.SetBoundsRect(gfx::Rect(gfx::Point(0, 0), Tab::GetStandardSize())); - - SkBitmap bitmap; - bitmap.setConfig(SkBitmap::kARGB_8888_Config, 16, 16); - bitmap.allocPixels(); - - TabRendererData data; - data.favicon = gfx::ImageSkia::CreateFrom1xBitmap(bitmap); - tab.SetData(data); - - // Recording starts and stops. - data.capture_state = TabRendererData::CAPTURE_STATE_RECORDING; - tab.SetData(data); - EXPECT_TRUE(IconAnimationInvariant(tab)); - EXPECT_EQ(TabRendererData::CAPTURE_STATE_RECORDING, tab.data().capture_state); - data.capture_state = TabRendererData::CAPTURE_STATE_NONE; - tab.SetData(data); - EXPECT_TRUE(IconAnimationInvariant(tab)); - EXPECT_EQ(TabRendererData::CAPTURE_STATE_NONE, tab.data().capture_state); - EXPECT_TRUE(IconAnimationInvariant(tab)); - - // Recording starts then tab capture starts, then back to just recording, then - // recording stops. - data.capture_state = TabRendererData::CAPTURE_STATE_RECORDING; - tab.SetData(data); - EXPECT_TRUE(IconAnimationInvariant(tab)); - EXPECT_EQ(TabRendererData::CAPTURE_STATE_RECORDING, tab.data().capture_state); - - data.title = ASCIIToUTF16("test X"); - tab.SetData(data); - EXPECT_TRUE(IconAnimationInvariant(tab)); - - data.capture_state = TabRendererData::CAPTURE_STATE_PROJECTING; - tab.SetData(data); - EXPECT_TRUE(IconAnimationInvariant(tab)); - EXPECT_EQ(TabRendererData::CAPTURE_STATE_PROJECTING, - tab.data().capture_state); - - data.title = ASCIIToUTF16("test Y"); - tab.SetData(data); - EXPECT_TRUE(IconAnimationInvariant(tab)); - - data.capture_state = TabRendererData::CAPTURE_STATE_RECORDING; - tab.SetData(data); - EXPECT_TRUE(IconAnimationInvariant(tab)); - EXPECT_EQ(TabRendererData::CAPTURE_STATE_RECORDING, tab.data().capture_state); - - data.capture_state = TabRendererData::CAPTURE_STATE_NONE; - tab.SetData(data); - EXPECT_TRUE(IconAnimationInvariant(tab)); - EXPECT_EQ(TabRendererData::CAPTURE_STATE_NONE, tab.data().capture_state); -} diff --git a/chrome/chrome_browser_ui.gypi b/chrome/chrome_browser_ui.gypi index 65f2e23..5fdfbdd 100644 --- a/chrome/chrome_browser_ui.gypi +++ b/chrome/chrome_browser_ui.gypi @@ -941,10 +941,10 @@ 'browser/ui/cocoa/tabpose_window.h', 'browser/ui/cocoa/tabpose_window.mm', 'browser/ui/cocoa/tabs/dock_info_mac.cc', + 'browser/ui/cocoa/tabs/media_indicator_view.h', + 'browser/ui/cocoa/tabs/media_indicator_view.mm', 'browser/ui/cocoa/tabs/tab_controller.h', 'browser/ui/cocoa/tabs/tab_controller.mm', - 'browser/ui/cocoa/tabs/tab_projecting_image_view.h', - 'browser/ui/cocoa/tabs/tab_projecting_image_view.mm', 'browser/ui/cocoa/tabs/tab_strip_controller.h', 'browser/ui/cocoa/tabs/tab_strip_controller.mm', 'browser/ui/cocoa/tabs/tab_strip_drag_controller.h', @@ -959,8 +959,6 @@ 'browser/ui/cocoa/tabs/tab_window_controller.mm', 'browser/ui/cocoa/tabs/throbber_view.h', 'browser/ui/cocoa/tabs/throbber_view.mm', - 'browser/ui/cocoa/tabs/throbbing_image_view.h', - 'browser/ui/cocoa/tabs/throbbing_image_view.mm', 'browser/ui/cocoa/task_manager_mac.h', 'browser/ui/cocoa/task_manager_mac.mm', 'browser/ui/cocoa/themed_window.h', diff --git a/chrome/chrome_tests_unit.gypi b/chrome/chrome_tests_unit.gypi index c0123e7..9c90800 100644 --- a/chrome/chrome_tests_unit.gypi +++ b/chrome/chrome_tests_unit.gypi @@ -1571,13 +1571,12 @@ 'browser/ui/cocoa/tab_contents/sad_tab_view_unittest.mm', 'browser/ui/cocoa/table_row_nsimage_cache_unittest.mm', 'browser/ui/cocoa/tabpose_window_unittest.mm', + 'browser/ui/cocoa/tabs/media_indicator_view_unittest.mm', 'browser/ui/cocoa/tabs/tab_controller_unittest.mm', - 'browser/ui/cocoa/tabs/tab_projecting_image_view_unittest.mm', 'browser/ui/cocoa/tabs/tab_strip_controller_unittest.mm', 'browser/ui/cocoa/tabs/tab_strip_view_unittest.mm', 'browser/ui/cocoa/tabs/tab_view_unittest.mm', 'browser/ui/cocoa/tabs/throbber_view_unittest.mm', - 'browser/ui/cocoa/tabs/throbbing_image_view_unittest.mm', 'browser/ui/cocoa/task_manager_mac_unittest.mm', 'browser/ui/cocoa/toolbar/reload_button_unittest.mm', 'browser/ui/cocoa/toolbar/toolbar_button_unittest.mm', |