From 39e5624b02994bf4bb5d3799136c2f39902695dd Mon Sep 17 00:00:00 2001 From: "avi@chromium.org" Date: Mon, 27 Jul 2009 16:34:17 +0000 Subject: Give crashed tabs the crashed tab icon on the Mac. BUG=none TEST=crash a page and see if the icon shows up (animated) Review URL: http://codereview.chromium.org/160113 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@21648 0039d316-1c4b-4281-b951-d872f2087c98 --- chrome/app/nibs/ClearBrowsingData.xib | 62 +++-- .../browser/cocoa/clear_browsing_data_controller.h | 1 - .../cocoa/clear_browsing_data_controller.mm | 10 - chrome/browser/cocoa/tab_controller.h | 1 + chrome/browser/cocoa/tab_strip_controller.mm | 15 +- chrome/browser/cocoa/throbber_view.h | 37 +-- chrome/browser/cocoa/throbber_view.mm | 260 ++++++++++++++++----- chrome/browser/cocoa/throbber_view_unittest.mm | 3 +- chrome/chrome.gyp | 1 + 9 files changed, 266 insertions(+), 124 deletions(-) diff --git a/chrome/app/nibs/ClearBrowsingData.xib b/chrome/app/nibs/ClearBrowsingData.xib index bb2a675..cb5229f 100644 --- a/chrome/app/nibs/ClearBrowsingData.xib +++ b/chrome/app/nibs/ClearBrowsingData.xib @@ -2,10 +2,10 @@ 1050 - 9F33 + 9J61 677 - 949.34 - 352.00 + 949.46 + 353.00 YES @@ -358,12 +358,15 @@ 25 - + - 268 - {{20, 24}, {16, 16}} + 1292 + + {{20, 20}, {16, 16}} - ThrobberView + 20746 + 1.600000e+01 + 1.000000e+02 {331, 299} @@ -372,6 +375,9 @@ {{0, 0}, {1680, 1028}} {3.40282e+38, 3.40282e+38} + + YES + @@ -576,10 +582,10 @@ hidden: isClearing - + - + hidden: isClearing hidden @@ -591,15 +597,7 @@ 2 - 70 - - - - progress_ - - - - 71 + 77 @@ -656,7 +654,7 @@ - + @@ -851,10 +849,15 @@ - 64 - + 72 + + + 73 + + + @@ -897,8 +900,8 @@ 4.IBPluginDependency 5.IBPluginDependency 6.IBPluginDependency - 64.IBPluginDependency 7.IBPluginDependency + 72.IBPluginDependency 8.IBPluginDependency @@ -964,7 +967,7 @@ - 71 + 77 @@ -985,10 +988,6 @@ id - - progress_ - ThrobberView - IBProjectSource browser/cocoa/clear_browsing_data_controller.h @@ -998,21 +997,20 @@ NSObject IBProjectSource - browser/cocoa/tab_strip_model_observer_bridge.h + browser/cocoa/status_bubble_mac.h - ThrobberView - NSView + NSObject IBProjectSource - browser/cocoa/throbber_view.h + browser/cocoa/tab_strip_model_observer_bridge.h 0 - ../../../chrome.xcodeproj + ../../chrome.xcodeproj 3 diff --git a/chrome/browser/cocoa/clear_browsing_data_controller.h b/chrome/browser/cocoa/clear_browsing_data_controller.h index d954981..3a8150b 100644 --- a/chrome/browser/cocoa/clear_browsing_data_controller.h +++ b/chrome/browser/cocoa/clear_browsing_data_controller.h @@ -27,7 +27,6 @@ class Profile; BrowsingDataRemover* remover_; scoped_ptr observer_; BOOL isClearing_; // YES while clearing data is ongoing. - IBOutlet ThrobberView* progress_; // Values for checkboxes, kept in sync with bindings. These values get // persisted into prefs if the user accepts the dialog. diff --git a/chrome/browser/cocoa/clear_browsing_data_controller.mm b/chrome/browser/cocoa/clear_browsing_data_controller.mm index 9910d28..03863f36 100644 --- a/chrome/browser/cocoa/clear_browsing_data_controller.mm +++ b/chrome/browser/cocoa/clear_browsing_data_controller.mm @@ -63,16 +63,6 @@ class ClearBrowsingObserver : public BrowsingDataRemover::Observer { [super dealloc]; } -// Called when outlets are available. Set the throbber icon. -- (void)awakeFromNib { - NSString *imagePath = [mac_util::MainAppBundle() - pathForResource:@"throbber" - ofType:@"png"]; - scoped_nsobject throbberImage( - [[NSImage alloc] initWithContentsOfFile:imagePath]); - [progress_ setImage:throbberImage]; -} - // Run application modal. - (void)runModalDialog { [[NSApplication sharedApplication] runModalForWindow:[self window]]; diff --git a/chrome/browser/cocoa/tab_controller.h b/chrome/browser/cocoa/tab_controller.h index ba1d626..bffdf7f 100644 --- a/chrome/browser/cocoa/tab_controller.h +++ b/chrome/browser/cocoa/tab_controller.h @@ -16,6 +16,7 @@ enum TabLoadingState { kTabDone, kTabLoading, kTabWaiting, + kTabCrashed, }; @class TabView; diff --git a/chrome/browser/cocoa/tab_strip_controller.mm b/chrome/browser/cocoa/tab_strip_controller.mm index efd4f1b..a15244f 100644 --- a/chrome/browser/cocoa/tab_strip_controller.mm +++ b/chrome/browser/cocoa/tab_strip_controller.mm @@ -571,6 +571,8 @@ static const float kUseFullAvailableWidth = -1.0; [nsimage_cache::ImageNamed(@"throbber_waiting.png") retain]; static NSImage* throbberLoadingImage = [nsimage_cache::ImageNamed(@"throbber.png") retain]; + static NSImage* sadFaviconImage = + [nsimage_cache::ImageNamed(@"sadfavicon.png") retain]; TabController* tabController = [tabArray_ objectAtIndex:index]; @@ -584,17 +586,24 @@ static const float kUseFullAvailableWidth = -1.0; } else if (contents->is_loading()) { newState = kTabLoading; throbberImage = throbberLoadingImage; + } else if (contents->is_crashed()) { + newState = kTabCrashed; } if (oldState != newState || newState == kTabDone) { NSView* iconView = nil; if (newState == kTabDone) { iconView = [self favIconImageViewForContents:contents]; + } else if (newState == kTabCrashed) { + NSImage* oldImage = [[self favIconImageViewForContents:contents] image]; + NSRect frame = NSMakeRect(0, 0, 16, 16); + iconView = [ThrobberView toastThrobberViewWithFrame:frame + beforeImage:oldImage + afterImage:sadFaviconImage]; } else { NSRect frame = NSMakeRect(0, 0, 16, 16); - iconView = - [[[ThrobberView alloc] initWithFrame:frame - image:throbberImage] autorelease]; + iconView = [ThrobberView filmstripThrobberViewWithFrame:frame + image:throbberImage]; } [tabController setLoadingState:newState]; diff --git a/chrome/browser/cocoa/throbber_view.h b/chrome/browser/cocoa/throbber_view.h index e0b06ba..ef334fb 100644 --- a/chrome/browser/cocoa/throbber_view.h +++ b/chrome/browser/cocoa/throbber_view.h @@ -9,32 +9,35 @@ #include "base/scoped_nsobject.h" -@class TimerTarget; +@class ThrobberTimerTarget; +@protocol ThrobberDataDelegate; // A class that knows how to draw an animated state to indicate progress. -// Currently draws via a sequence of frames in an image, but may ultimately be -// written to render paths that are rotated around a center origin. Creating -// the class starts the animation, destroying it stops it. There is no state -// where the class is frozen on an image and not animating. +// Creating the class starts the animation, destroying it stops it. There are +// two types: +// +// - Filmstrip: Draws via a sequence of frames in an image. There is no state +// where the class is frozen on an image and not animating. The image needs to +// be made of squares such that the height divides evently into the width. +// +// - Toast: Draws an image animating down to the bottom and then another image +// animating up from the bottom. Stops once the animation is complete. @interface ThrobberView : NSView { @private - scoped_nsobject image_; - scoped_nsobject target_; // Target of animation timer. + scoped_nsobject target_; // Target of animation timer. + id dataDelegate_; NSTimer* timer_; // Animation timer. Weak, owned by runloop. - unsigned int numFrames_; // Number of frames in this animation. - unsigned int animationFrame_; // Current frame of the animation, - // [0..numFrames_) } -// Creates the view with |frame| and the image strip desginated by |image|. The -// image needs to be made of squares such that the height divides evently into -// the width. Takes ownership of |image|. -- (id)initWithFrame:(NSRect)frame image:(NSImage*)image; +// Creates a filmstrip view with |frame| and image |image|. ++ (id)filmstripThrobberViewWithFrame:(NSRect)frame + image:(NSImage*)image; -// Allows changing the image once the view has been created, such as when the -// view is loaded from a nib. The same restrictions as above apply. -- (void)setImage:(NSImage*)image; +// Creates a toast view with |frame| and specified images. ++ (id)toastThrobberViewWithFrame:(NSRect)frame + beforeImage:(NSImage*)beforeImage + afterImage:(NSImage*)afterImage; @end diff --git a/chrome/browser/cocoa/throbber_view.mm b/chrome/browser/cocoa/throbber_view.mm index bfdf4bb..696ada7 100644 --- a/chrome/browser/cocoa/throbber_view.mm +++ b/chrome/browser/cocoa/throbber_view.mm @@ -9,20 +9,171 @@ static const float kAnimationIntervalSeconds = 0.03; // 30ms, same as windows @interface ThrobberView(PrivateMethods) +- (id)initWithFrame:(NSRect)frame delegate:(id)delegate; - (void)animate; @end +@protocol ThrobberDataDelegate +// Is the current frame the last frame of the animation? +- (BOOL)animationIsComplete; + +// Draw the current frame into the current graphics context. +- (void)drawFrameInRect:(NSRect)rect; + +// Update the frame counter. +- (void)advanceFrame; +@end + +@interface ThrobberFilmstripDelegate : NSObject + { + scoped_nsobject image_; + unsigned int numFrames_; // Number of frames in this animation. + unsigned int animationFrame_; // Current frame of the animation, + // [0..numFrames_) +} + +- (id)initWithImage:(NSImage*)image; + +@end + +@implementation ThrobberFilmstripDelegate + +// Stores the internal representation of the image from |image|. We use +// CoreImage for speed (though this doesn't seem to help perf issues). We +// validate that the image is of the appropriate ratio. +- (id)initWithImage:(NSImage*)image { + if ((self = [super init])) { + // Reset the animation counter so there's no chance we are off the end. + animationFrame_ = 0; + + // Ensure that the height divides evenly into the width. Cache the + // number of frames in the animation for later. + NSSize imageSize = [image size]; + DCHECK(imageSize.height && imageSize.width); + if (!imageSize.height) + return nil; + DCHECK((int)imageSize.width % (int)imageSize.height == 0); + numFrames_ = (int)imageSize.width / (int)imageSize.height; + DCHECK(numFrames_); + + // First check if we have a bitmap image rep and use it, otherwise fall + // back to creating one. + NSBitmapImageRep* rep = [[image representations] objectAtIndex:0]; + if (![rep isKindOfClass:[NSBitmapImageRep class]]) { + [image lockFocus]; + NSRect imageRect = NSMakeRect(0, 0, imageSize.width, imageSize.height); + rep = [[[NSBitmapImageRep alloc] initWithFocusedViewRect:imageRect] + autorelease]; + [image unlockFocus]; + } + image_.reset([[CIImage alloc] initWithBitmapImageRep:rep]); + } + return self; +} + +- (BOOL)animationIsComplete { + return NO; +} + +- (void)drawFrameInRect:(NSRect)rect { + float imageDimension = [image_ extent].size.height; + float xOffset = animationFrame_ * imageDimension; + NSRect sourceImageRect = + NSMakeRect(xOffset, 0, imageDimension, imageDimension); + [image_ drawInRect:rect + fromRect:sourceImageRect + operation:NSCompositeSourceOver + fraction:1.0]; +} + +- (void)advanceFrame { + animationFrame_ = ++animationFrame_ % numFrames_; +} + +@end + +@interface ThrobberToastDelegate : NSObject + { + scoped_nsobject image1_; + scoped_nsobject image2_; + NSSize image1Size_; + NSSize image2Size_; + int animationFrame_; // Current frame of the animation, +} + +- (id)initWithImage1:(NSImage*)image1 image2:(NSImage*)image2; + +@end + +@implementation ThrobberToastDelegate + +- (id)initWithImage1:(NSImage*)image1 image2:(NSImage*)image2 { + if ((self = [super init])) { + image1_.reset([image1 retain]); + image2_.reset([image2 retain]); + image1Size_ = [image1 size]; + image2Size_ = [image2 size]; + animationFrame_ = 0; + } + return self; +} + +- (BOOL)animationIsComplete { + if (animationFrame_ >= image1Size_.height + image2Size_.height) + return YES; + + return NO; +} + +// From [0..image1Height) we draw image1, at image1Height we draw nothing, and +// from [image1Height+1..image1Hight+image2Height] we draw the second image. +- (void)drawFrameInRect:(NSRect)rect { + NSImage* image = nil; + NSSize srcSize; + NSRect destRect; + + if (animationFrame_ < image1Size_.height) { + image = image1_.get(); + srcSize = image1Size_; + destRect = NSMakeRect(0, -animationFrame_, + image1Size_.width, image1Size_.height); + } else if (animationFrame_ == image1Size_.height) { + // nothing; intermediate blank frame + } else { + image = image2_.get(); + srcSize = image2Size_; + destRect = NSMakeRect(0, animationFrame_ - + (image1Size_.height + image2Size_.height), + image2Size_.width, image2Size_.height); + } + + if (image) { + NSRect sourceImageRect = + NSMakeRect(0, 0, srcSize.width, srcSize.height); + [image drawInRect:destRect + fromRect:sourceImageRect + operation:NSCompositeSourceOver + fraction:1.0]; + } +} + +- (void)advanceFrame { + ++animationFrame_; +} + +@end + // A very simple object that is the target for the animation timer so that // the view isn't. We do this to avoid retain cycles as the timer // retains its target. -@interface TimerTarget : NSObject { +@interface ThrobberTimerTarget : NSObject { @private ThrobberView* throbber_; // Weak, owns us } - (id)initWithThrobber:(ThrobberView*)view; @end -@implementation TimerTarget +@implementation ThrobberTimerTarget - (id)initWithThrobber:(ThrobberView*)view { if ((self = [super init])) { throbber_ = view; @@ -37,15 +188,50 @@ static const float kAnimationIntervalSeconds = 0.03; // 30ms, same as windows @implementation ThrobberView -- (id)initWithFrame:(NSRect)frame image:(NSImage*)image { ++ (id)filmstripThrobberViewWithFrame:(NSRect)frame + image:(NSImage*)image { + ThrobberFilmstripDelegate* delegate = + [[[ThrobberFilmstripDelegate alloc] initWithImage:image] autorelease]; + if (!delegate) + return nil; + + return [[[ThrobberView alloc] initWithFrame:frame + delegate:delegate] autorelease]; +} + ++ (id)toastThrobberViewWithFrame:(NSRect)frame + beforeImage:(NSImage*)beforeImage + afterImage:(NSImage*)afterImage { + ThrobberToastDelegate* delegate = + [[[ThrobberToastDelegate alloc] initWithImage1:beforeImage + image2:afterImage] autorelease]; + if (!delegate) + return nil; + + return [[[ThrobberView alloc] initWithFrame:frame + delegate:delegate] autorelease]; +} + +- (id)initWithFrame:(NSRect)frame delegate:(id)delegate { if ((self = [super initWithFrame:frame])) { - [self setImage:image]; + dataDelegate_ = [delegate retain]; + + // Start a timer for the animation frames. + target_.reset([[ThrobberTimerTarget alloc] initWithThrobber:self]); + timer_ = [NSTimer scheduledTimerWithTimeInterval:kAnimationIntervalSeconds + target:target_.get() + selector:@selector(animate:) + userInfo:nil + repeats:YES]; } return self; } - (void)dealloc { - [timer_ invalidate]; + [dataDelegate_ release]; + if (timer_) + [timer_ invalidate]; + [super dealloc]; } @@ -56,67 +242,21 @@ static const float kAnimationIntervalSeconds = 0.03; // 30ms, same as windows [super removeFromSuperview]; } -// Called when the TimerTarget gets tickled by our timer. Increment the frame -// counter and mark as needing display. +// Called when the ThrobberTimerTarget gets tickled by our timer. Advance the +// frame, dirty the display, and kill the timer when it's no longer needed. - (void)animate { - animationFrame_ = ++animationFrame_ % numFrames_; + [dataDelegate_ advanceFrame]; [self setNeedsDisplay:YES]; + + if ([dataDelegate_ animationIsComplete]) { + [timer_ invalidate]; + timer_ = nil; + } } // Overridden to draw the appropriate frame in the image strip. - (void)drawRect:(NSRect)rect { - float imageDimension = [image_ extent].size.height; - float xOffset = animationFrame_ * imageDimension; - NSRect sourceImageRect = - NSMakeRect(xOffset, 0, imageDimension, imageDimension); - [image_ drawInRect:[self bounds] - fromRect:sourceImageRect - operation:NSCompositeSourceOver - fraction:1.0]; -} - -// Stores the internal representation of the image from |image|. We use -// CoreImage for speed (though this doesn't seem to help perf issues). We -// validate that the image is of the appropriate ratio. If the image has more -// than one frame, restarts the timer. -- (void)setImage:(NSImage*)image { - // Reset the animation counter so there's no chance we are off the end. - animationFrame_ = 0; - [timer_ invalidate]; - timer_ = nil; - - // Ensure that the height divides evenly into the width. Cache the - // number of frames in the animation for later. - NSSize imageSize = [image size]; - DCHECK(imageSize.height && imageSize.width); - if (!imageSize.height) - return; - DCHECK((int)imageSize.width % (int)imageSize.height == 0); - numFrames_ = (int)imageSize.width / (int)imageSize.height; - DCHECK(numFrames_); - - // First check if we have a bitmap image rep and use it, otherwise fall - // back to creating one. - NSBitmapImageRep* rep = [[image representations] objectAtIndex:0]; - if (![rep isKindOfClass:[NSBitmapImageRep class]]) { - [image lockFocus]; - NSRect imageRect = NSMakeRect(0, 0, imageSize.width, imageSize.height); - rep = [[[NSBitmapImageRep alloc] initWithFocusedViewRect:imageRect] - autorelease]; - [image unlockFocus]; - } - image_.reset([[CIImage alloc] initWithBitmapImageRep:rep]); - - if (numFrames_ > 1) { - // Start a timer for the animation frames. - target_.reset([[TimerTarget alloc] initWithThrobber:self]); - timer_ = - [NSTimer scheduledTimerWithTimeInterval:kAnimationIntervalSeconds - target:target_.get() - selector:@selector(animate:) - userInfo:nil - repeats:YES]; - } + [dataDelegate_ drawFrameInRect:[self bounds]]; } @end diff --git a/chrome/browser/cocoa/throbber_view_unittest.mm b/chrome/browser/cocoa/throbber_view_unittest.mm index 72d2240..2cf2db4 100644 --- a/chrome/browser/cocoa/throbber_view_unittest.mm +++ b/chrome/browser/cocoa/throbber_view_unittest.mm @@ -20,7 +20,8 @@ class ThrobberViewTest : public PlatformTest { NSImage* image = [[[NSImage alloc] initByReferencingFile: [bundle pathForResource:@"throbber" ofType:@"png"]] autorelease]; - view_.reset([[ThrobberView alloc] initWithFrame:frame image:image]); + view_.reset([[ThrobberView filmstripThrobberViewWithFrame:frame + image:image] retain]); [cocoa_helper_.contentView() addSubview:view_.get()]; } diff --git a/chrome/chrome.gyp b/chrome/chrome.gyp index e94ca05..f404291 100644 --- a/chrome/chrome.gyp +++ b/chrome/chrome.gyp @@ -2699,6 +2699,7 @@ 'app/theme/pageinfo_bad.png', 'app/theme/pageinfo_good.png', 'app/theme/reload_Template.pdf', + 'app/theme/sadfavicon.png', 'app/theme/sadtab.png', 'app/theme/star_Template.pdf', 'app/theme/starred.pdf', -- cgit v1.1