1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 #include "chrome/browser/ui/cocoa/notifications/balloon_controller.h" 6 7 #include "base/mac/bundle_locations.h" 8 #include "base/mac/mac_util.h" 9 #include "base/strings/utf_string_conversions.h" 10 #include "chrome/browser/notifications/balloon.h" 11 #include "chrome/browser/notifications/desktop_notification_service.h" 12 #include "chrome/browser/notifications/desktop_notification_service_factory.h" 13 #include "chrome/browser/notifications/notification.h" 14 #include "chrome/browser/notifications/notification_options_menu_model.h" 15 #include "chrome/browser/profiles/profile.h" 16 #import "chrome/browser/ui/cocoa/notifications/balloon_view.h" 17 #include "chrome/browser/ui/cocoa/notifications/balloon_view_host_mac.h" 18 #include "content/public/browser/render_view_host.h" 19 #include "content/public/browser/web_contents.h" 20 #include "grit/generated_resources.h" 21 #include "grit/theme_resources.h" 22 #import "ui/base/cocoa/hover_image_button.h" 23 #import "ui/base/cocoa/menu_controller.h" 24 #include "ui/base/l10n/l10n_util.h" 25 #include "ui/base/resource/resource_bundle.h" 26 27 namespace { 28 29 // Margin, in pixels, between the notification frame and the contents 30 // of the notification. 31 const int kTopMargin = 1; 32 const int kBottomMargin = 2; 33 const int kLeftMargin = 2; 34 const int kRightMargin = 2; 35 36 } // namespace 37 38 @interface BalloonController (Private) 39 - (void)updateTrackingRect; 40 @end 41 42 @implementation BalloonController 43 44 - (id)initWithBalloon:(Balloon*)balloon { 45 NSString* nibpath = 46 [base::mac::FrameworkBundle() pathForResource:@"Notification" 47 ofType:@"nib"]; 48 if ((self = [super initWithWindowNibPath:nibpath owner:self])) { 49 balloon_ = balloon; 50 [self initializeHost]; 51 menuModel_.reset(new NotificationOptionsMenuModel(balloon)); 52 menuController_.reset([[MenuController alloc] initWithModel:menuModel_.get() 53 useWithPopUpButtonCell:NO]); 54 } 55 return self; 56 } 57 58 - (void)awakeFromNib { 59 DCHECK([self window]); 60 DCHECK_EQ(self, [[self window] delegate]); 61 62 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 63 [optionsButton_ setDefaultImage: 64 rb.GetNativeImageNamed(IDR_BALLOON_WRENCH).ToNSImage()]; 65 [optionsButton_ setHoverImage: 66 rb.GetNativeImageNamed(IDR_BALLOON_WRENCH_H).ToNSImage()]; 67 [optionsButton_ setPressedImage: 68 rb.GetNativeImageNamed(IDR_BALLOON_WRENCH_P).ToNSImage()]; 69 [[optionsButton_ cell] setHighlightsBy:NSNoCellMask]; 70 71 NSString* sourceLabelText = l10n_util::GetNSStringF( 72 IDS_NOTIFICATION_BALLOON_SOURCE_LABEL, 73 balloon_->notification().display_source()); 74 [originLabel_ setStringValue:sourceLabelText]; 75 76 // This condition is false in unit tests which have no RVH. 77 if (htmlContents_.get()) { 78 gfx::NativeView contents = htmlContents_->native_view(); 79 [contents setFrame:NSMakeRect(kLeftMargin, kTopMargin, 0, 0)]; 80 [[htmlContainer_ superview] addSubview:contents 81 positioned:NSWindowBelow 82 relativeTo:nil]; 83 } 84 85 // Use the standard close button for a utility window. 86 closeButton_ = [NSWindow standardWindowButton:NSWindowCloseButton 87 forStyleMask:NSUtilityWindowMask]; 88 NSRect frame = [closeButton_ frame]; 89 [closeButton_ setFrame:NSMakeRect(6, 1, frame.size.width, frame.size.height)]; 90 [closeButton_ setTarget:self]; 91 [closeButton_ setAction:@selector(closeButtonPressed:)]; 92 [shelf_ addSubview:closeButton_]; 93 [self updateTrackingRect]; 94 95 // Set the initial position without animating (the balloon should not 96 // yet be visible). 97 DCHECK(![[self window] isVisible]); 98 NSRect balloon_frame = NSMakeRect(balloon_->GetPosition().x(), 99 balloon_->GetPosition().y(), 100 [self desiredTotalWidth], 101 [self desiredTotalHeight]); 102 [[self window] setFrame:balloon_frame 103 display:NO]; 104 } 105 106 - (void)updateTrackingRect { 107 if (closeButtonTrackingTag_) 108 [shelf_ removeTrackingRect:closeButtonTrackingTag_]; 109 110 closeButtonTrackingTag_ = [shelf_ addTrackingRect:[closeButton_ frame] 111 owner:self 112 userData:nil 113 assumeInside:NO]; 114 } 115 116 - (void) mouseEntered:(NSEvent*)event { 117 [[closeButton_ cell] setHighlighted:YES]; 118 } 119 120 - (void) mouseExited:(NSEvent*)event { 121 [[closeButton_ cell] setHighlighted:NO]; 122 } 123 124 - (void)closeBalloonNow:(bool)byUser { 125 if (!balloon_) 126 return; 127 [self close]; 128 if (htmlContents_.get()) 129 htmlContents_->Shutdown(); 130 if (balloon_) 131 balloon_->OnClose(byUser); 132 balloon_ = NULL; 133 } 134 135 - (IBAction)optionsButtonPressed:(id)sender { 136 optionMenuIsActive_ = YES; 137 [NSMenu popUpContextMenu:[menuController_ menu] 138 withEvent:[NSApp currentEvent] 139 forView:optionsButton_]; 140 optionMenuIsActive_ = NO; 141 if (delayedClose_) 142 [self closeBalloonNow: false]; // always by script. 143 } 144 145 - (IBAction)permissionRevoked:(id)sender { 146 DesktopNotificationService* service = 147 DesktopNotificationServiceFactory::GetForProfile(balloon_->profile()); 148 service->DenyPermission(balloon_->notification().origin_url()); 149 } 150 151 - (IBAction)closeButtonPressed:(id)sender { 152 [self closeBalloon:YES]; 153 [self close]; 154 } 155 156 - (void)close { 157 if (closeButtonTrackingTag_) 158 [shelf_ removeTrackingRect:closeButtonTrackingTag_]; 159 160 [super close]; 161 } 162 163 - (void)closeBalloon:(bool)byUser { 164 // Keep alive while user is interacting with popup menu. 165 // Otherwise the script can close the notification and underlying balloon 166 // will be destroyed while user select a menu command. 167 if (!byUser && optionMenuIsActive_) { 168 delayedClose_ = YES; 169 return; 170 } 171 [self closeBalloonNow: byUser]; 172 } 173 174 - (void)updateContents { 175 DCHECK(htmlContents_.get()) << "BalloonView::Update called before Show"; 176 if (htmlContents_->web_contents()) { 177 htmlContents_->web_contents()->GetController().LoadURL( 178 balloon_->notification().content_url(), content::Referrer(), 179 content::PAGE_TRANSITION_LINK, std::string()); 180 } 181 } 182 183 - (void)repositionToBalloon { 184 DCHECK(balloon_); 185 int x = balloon_->GetPosition().x(); 186 int y = balloon_->GetPosition().y(); 187 int w = [self desiredTotalWidth]; 188 int h = [self desiredTotalHeight]; 189 190 [[self window] setFrame:NSMakeRect(x, y, w, h) 191 display:YES]; 192 if (htmlContents_.get()) 193 htmlContents_->UpdateActualSize(balloon_->content_size()); 194 } 195 196 // Returns the total width the view should be to accommodate the balloon. 197 - (int)desiredTotalWidth { 198 return (balloon_ ? balloon_->content_size().width() : 0) + 199 kLeftMargin + kRightMargin; 200 } 201 202 // Returns the total height the view should be to accommodate the balloon. 203 - (int)desiredTotalHeight { 204 return (balloon_ ? balloon_->content_size().height() : 0) + 205 kTopMargin + kBottomMargin + [shelf_ frame].size.height; 206 } 207 208 // Returns the BalloonHost { 209 - (BalloonViewHost*) getHost { 210 return htmlContents_.get(); 211 } 212 213 // Initializes the renderer host showing the HTML contents. 214 - (void)initializeHost { 215 htmlContents_.reset(new BalloonViewHost(balloon_)); 216 htmlContents_->Init(); 217 } 218 219 // NSWindowDelegate notification. 220 - (void)windowWillClose:(NSNotification*)notif { 221 [self autorelease]; 222 } 223 224 @end 225