1 // Copyright (c) 2011 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 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_bubble_controller.h" 6 7 #include "base/mac/mac_util.h" 8 #include "base/sys_string_conversions.h" 9 #include "base/utf_string_conversions.h" // TODO(viettrungluu): remove 10 #include "chrome/browser/bookmarks/bookmark_model.h" 11 #include "chrome/browser/metrics/user_metrics.h" 12 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_button.h" 13 #import "chrome/browser/ui/cocoa/browser_window_controller.h" 14 #import "chrome/browser/ui/cocoa/info_bubble_view.h" 15 #include "content/common/notification_observer.h" 16 #include "content/common/notification_registrar.h" 17 #include "content/common/notification_service.h" 18 #include "grit/generated_resources.h" 19 #include "ui/base/l10n/l10n_util_mac.h" 20 21 22 // Simple class to watch for tab creation/destruction and close the bubble. 23 // Bridge between Chrome-style notifications and ObjC-style notifications. 24 class BookmarkBubbleNotificationBridge : public NotificationObserver { 25 public: 26 BookmarkBubbleNotificationBridge(BookmarkBubbleController* controller, 27 SEL selector); 28 virtual ~BookmarkBubbleNotificationBridge() {} 29 void Observe(NotificationType type, 30 const NotificationSource& source, 31 const NotificationDetails& details); 32 private: 33 NotificationRegistrar registrar_; 34 BookmarkBubbleController* controller_; // weak; owns us. 35 SEL selector_; // SEL sent to controller_ on notification. 36 }; 37 38 BookmarkBubbleNotificationBridge::BookmarkBubbleNotificationBridge( 39 BookmarkBubbleController* controller, SEL selector) 40 : controller_(controller), selector_(selector) { 41 // registrar_ will automatically RemoveAll() when destroyed so we 42 // don't need to do so explicitly. 43 registrar_.Add(this, NotificationType::TAB_CONTENTS_CONNECTED, 44 NotificationService::AllSources()); 45 registrar_.Add(this, NotificationType::TAB_CLOSED, 46 NotificationService::AllSources()); 47 } 48 49 // At this time all notifications instigate the same behavior (go 50 // away) so we don't bother checking which notification came in. 51 void BookmarkBubbleNotificationBridge::Observe( 52 NotificationType type, 53 const NotificationSource& source, 54 const NotificationDetails& details) { 55 [controller_ performSelector:selector_ withObject:controller_]; 56 } 57 58 59 // An object to represent the ChooseAnotherFolder item in the pop up. 60 @interface ChooseAnotherFolder : NSObject 61 @end 62 63 @implementation ChooseAnotherFolder 64 @end 65 66 @interface BookmarkBubbleController (PrivateAPI) 67 - (void)updateBookmarkNode; 68 - (void)fillInFolderList; 69 - (void)parentWindowWillClose:(NSNotification*)notification; 70 @end 71 72 @implementation BookmarkBubbleController 73 74 @synthesize node = node_; 75 76 + (id)chooseAnotherFolderObject { 77 // Singleton object to act as a representedObject for the "choose another 78 // folder" item in the pop up. 79 static ChooseAnotherFolder* object = nil; 80 if (!object) { 81 object = [[ChooseAnotherFolder alloc] init]; 82 } 83 return object; 84 } 85 86 - (id)initWithParentWindow:(NSWindow*)parentWindow 87 model:(BookmarkModel*)model 88 node:(const BookmarkNode*)node 89 alreadyBookmarked:(BOOL)alreadyBookmarked { 90 NSString* nibPath = 91 [base::mac::MainAppBundle() pathForResource:@"BookmarkBubble" 92 ofType:@"nib"]; 93 if ((self = [super initWithWindowNibPath:nibPath owner:self])) { 94 parentWindow_ = parentWindow; 95 model_ = model; 96 node_ = node; 97 alreadyBookmarked_ = alreadyBookmarked; 98 99 // Watch to see if the parent window closes, and if so, close this one. 100 NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; 101 [center addObserver:self 102 selector:@selector(parentWindowWillClose:) 103 name:NSWindowWillCloseNotification 104 object:parentWindow_]; 105 } 106 return self; 107 } 108 109 - (void)dealloc { 110 [[NSNotificationCenter defaultCenter] removeObserver:self]; 111 [super dealloc]; 112 } 113 114 // If this is a new bookmark somewhere visible (e.g. on the bookmark 115 // bar), pulse it. Else, call ourself recursively with our parent 116 // until we find something visible to pulse. 117 - (void)startPulsingBookmarkButton:(const BookmarkNode*)node { 118 while (node) { 119 if ((node->parent() == model_->GetBookmarkBarNode()) || 120 (node == model_->other_node())) { 121 pulsingBookmarkNode_ = node; 122 NSValue *value = [NSValue valueWithPointer:node]; 123 NSDictionary *dict = [NSDictionary 124 dictionaryWithObjectsAndKeys:value, 125 bookmark_button::kBookmarkKey, 126 [NSNumber numberWithBool:YES], 127 bookmark_button::kBookmarkPulseFlagKey, 128 nil]; 129 [[NSNotificationCenter defaultCenter] 130 postNotificationName:bookmark_button::kPulseBookmarkButtonNotification 131 object:self 132 userInfo:dict]; 133 return; 134 } 135 node = node->parent(); 136 } 137 } 138 139 - (void)stopPulsingBookmarkButton { 140 if (!pulsingBookmarkNode_) 141 return; 142 NSValue *value = [NSValue valueWithPointer:pulsingBookmarkNode_]; 143 pulsingBookmarkNode_ = NULL; 144 NSDictionary *dict = [NSDictionary 145 dictionaryWithObjectsAndKeys:value, 146 bookmark_button::kBookmarkKey, 147 [NSNumber numberWithBool:NO], 148 bookmark_button::kBookmarkPulseFlagKey, 149 nil]; 150 [[NSNotificationCenter defaultCenter] 151 postNotificationName:bookmark_button::kPulseBookmarkButtonNotification 152 object:self 153 userInfo:dict]; 154 } 155 156 // Close the bookmark bubble without changing anything. Unlike a 157 // typical dialog's OK/Cancel, where Cancel is "do nothing", all 158 // buttons on the bubble have the capacity to change the bookmark 159 // model. This is an IBOutlet-looking entry point to remove the 160 // dialog without touching the model. 161 - (void)dismissWithoutEditing:(id)sender { 162 [self close]; 163 } 164 165 - (void)parentWindowWillClose:(NSNotification*)notification { 166 [self close]; 167 } 168 169 - (void)windowWillClose:(NSNotification*)notification { 170 // We caught a close so we don't need to watch for the parent closing. 171 [[NSNotificationCenter defaultCenter] removeObserver:self]; 172 bookmark_observer_.reset(NULL); 173 chrome_observer_.reset(NULL); 174 [self stopPulsingBookmarkButton]; 175 [self autorelease]; 176 } 177 178 // We want this to be a child of a browser window. addChildWindow: 179 // (called from this function) will bring the window on-screen; 180 // unfortunately, [NSWindowController showWindow:] will also bring it 181 // on-screen (but will cause unexpected changes to the window's 182 // position). We cannot have an addChildWindow: and a subsequent 183 // showWindow:. Thus, we have our own version. 184 - (void)showWindow:(id)sender { 185 BrowserWindowController* bwc = 186 [BrowserWindowController browserWindowControllerForWindow:parentWindow_]; 187 [bwc lockBarVisibilityForOwner:self withAnimation:NO delay:NO]; 188 NSWindow* window = [self window]; // completes nib load 189 [bubble_ setArrowLocation:info_bubble::kTopRight]; 190 // Insure decent positioning even in the absence of a browser controller, 191 // which will occur for some unit tests. 192 NSPoint arrowtip = bwc ? [bwc bookmarkBubblePoint] : 193 NSMakePoint([window frame].size.width, [window frame].size.height); 194 NSPoint origin = [parentWindow_ convertBaseToScreen:arrowtip]; 195 NSPoint bubbleArrowtip = [bubble_ arrowTip]; 196 bubbleArrowtip = [bubble_ convertPoint:bubbleArrowtip toView:nil]; 197 origin.y -= bubbleArrowtip.y; 198 origin.x -= bubbleArrowtip.x; 199 [window setFrameOrigin:origin]; 200 [parentWindow_ addChildWindow:window ordered:NSWindowAbove]; 201 // Default is IDS_BOOMARK_BUBBLE_PAGE_BOOKMARK; "Bookmark". 202 // If adding for the 1st time the string becomes "Bookmark Added!" 203 if (!alreadyBookmarked_) { 204 NSString* title = 205 l10n_util::GetNSString(IDS_BOOMARK_BUBBLE_PAGE_BOOKMARKED); 206 [bigTitle_ setStringValue:title]; 207 } 208 209 [self fillInFolderList]; 210 211 // Ping me when things change out from under us. Unlike a normal 212 // dialog, the bookmark bubble's cancel: means "don't add this as a 213 // bookmark", not "cancel editing". We must take extra care to not 214 // touch the bookmark in this selector. 215 bookmark_observer_.reset(new BookmarkModelObserverForCocoa( 216 node_, model_, 217 self, 218 @selector(dismissWithoutEditing:))); 219 chrome_observer_.reset(new BookmarkBubbleNotificationBridge( 220 self, @selector(dismissWithoutEditing:))); 221 222 // Pulse something interesting on the bookmark bar. 223 [self startPulsingBookmarkButton:node_]; 224 225 [window makeKeyAndOrderFront:self]; 226 } 227 228 - (void)close { 229 [[BrowserWindowController browserWindowControllerForWindow:parentWindow_] 230 releaseBarVisibilityForOwner:self withAnimation:YES delay:NO]; 231 [parentWindow_ removeChildWindow:[self window]]; 232 233 // If you quit while the bubble is open, sometimes we get a 234 // DidResignKey before we get our parent's WindowWillClose and 235 // sometimes not. We protect against a multiple close (or reference 236 // to parentWindow_ at a bad time) by clearing it out once we're 237 // done, and by removing ourself from future notifications. 238 [[NSNotificationCenter defaultCenter] 239 removeObserver:self 240 name:NSWindowWillCloseNotification 241 object:parentWindow_]; 242 parentWindow_ = nil; 243 244 [super close]; 245 } 246 247 // Shows the bookmark editor sheet for more advanced editing. 248 - (void)showEditor { 249 [self ok:self]; 250 // Send the action up through the responder chain. 251 [NSApp sendAction:@selector(editBookmarkNode:) to:nil from:self]; 252 } 253 254 - (IBAction)edit:(id)sender { 255 UserMetrics::RecordAction(UserMetricsAction("BookmarkBubble_Edit"), 256 model_->profile()); 257 [self showEditor]; 258 } 259 260 - (IBAction)ok:(id)sender { 261 [self stopPulsingBookmarkButton]; // before parent changes 262 [self updateBookmarkNode]; 263 [self close]; 264 } 265 266 // By implementing this, ESC causes the window to go away. If clicking the 267 // star was what prompted this bubble to appear (i.e., not already bookmarked), 268 // remove the bookmark. 269 - (IBAction)cancel:(id)sender { 270 if (!alreadyBookmarked_) { 271 // |-remove:| calls |-close| so don't do it. 272 [self remove:sender]; 273 } else { 274 [self ok:sender]; 275 } 276 } 277 278 - (IBAction)remove:(id)sender { 279 [self stopPulsingBookmarkButton]; 280 // TODO(viettrungluu): get rid of conversion and utf_string_conversions.h. 281 model_->SetURLStarred(node_->GetURL(), node_->GetTitle(), false); 282 UserMetrics::RecordAction(UserMetricsAction("BookmarkBubble_Unstar"), 283 model_->profile()); 284 node_ = NULL; // no longer valid 285 [self ok:sender]; 286 } 287 288 // The controller is the target of the pop up button box action so it can 289 // handle when "choose another folder" was picked. 290 - (IBAction)folderChanged:(id)sender { 291 DCHECK([sender isEqual:folderPopUpButton_]); 292 // It is possible that due to model change our parent window has been closed 293 // but the popup is still showing and able to notify the controller of a 294 // folder change. We ignore the sender in this case. 295 if (!parentWindow_) 296 return; 297 NSMenuItem* selected = [folderPopUpButton_ selectedItem]; 298 ChooseAnotherFolder* chooseItem = [[self class] chooseAnotherFolderObject]; 299 if ([[selected representedObject] isEqual:chooseItem]) { 300 UserMetrics::RecordAction( 301 UserMetricsAction("BookmarkBubble_EditFromCombobox"), 302 model_->profile()); 303 [self showEditor]; 304 } 305 } 306 307 // The controller is the delegate of the window so it receives did resign key 308 // notifications. When key is resigned mirror Windows behavior and close the 309 // window. 310 - (void)windowDidResignKey:(NSNotification*)notification { 311 NSWindow* window = [self window]; 312 DCHECK_EQ([notification object], window); 313 if ([window isVisible]) { 314 // If the window isn't visible, it is already closed, and this notification 315 // has been sent as part of the closing operation, so no need to close. 316 [self ok:self]; 317 } 318 } 319 320 // Look at the dialog; if the user has changed anything, update the 321 // bookmark node to reflect this. 322 - (void)updateBookmarkNode { 323 if (!node_) return; 324 325 // First the title... 326 NSString* oldTitle = base::SysUTF16ToNSString(node_->GetTitle()); 327 NSString* newTitle = [nameTextField_ stringValue]; 328 if (![oldTitle isEqual:newTitle]) { 329 model_->SetTitle(node_, base::SysNSStringToUTF16(newTitle)); 330 UserMetrics::RecordAction( 331 UserMetricsAction("BookmarkBubble_ChangeTitleInBubble"), 332 model_->profile()); 333 } 334 // Then the parent folder. 335 const BookmarkNode* oldParent = node_->parent(); 336 NSMenuItem* selectedItem = [folderPopUpButton_ selectedItem]; 337 id representedObject = [selectedItem representedObject]; 338 if ([representedObject isEqual:[[self class] chooseAnotherFolderObject]]) { 339 // "Choose another folder..." 340 return; 341 } 342 const BookmarkNode* newParent = 343 static_cast<const BookmarkNode*>([representedObject pointerValue]); 344 DCHECK(newParent); 345 if (oldParent != newParent) { 346 int index = newParent->child_count(); 347 model_->Move(node_, newParent, index); 348 UserMetrics::RecordAction(UserMetricsAction("BookmarkBubble_ChangeParent"), 349 model_->profile()); 350 } 351 } 352 353 // Fill in all information related to the folder pop up button. 354 - (void)fillInFolderList { 355 [nameTextField_ setStringValue:base::SysUTF16ToNSString(node_->GetTitle())]; 356 DCHECK([folderPopUpButton_ numberOfItems] == 0); 357 [self addFolderNodes:model_->root_node() 358 toPopUpButton:folderPopUpButton_ 359 indentation:0]; 360 NSMenu* menu = [folderPopUpButton_ menu]; 361 NSString* title = [[self class] chooseAnotherFolderString]; 362 NSMenuItem *item = [menu addItemWithTitle:title 363 action:NULL 364 keyEquivalent:@""]; 365 ChooseAnotherFolder* obj = [[self class] chooseAnotherFolderObject]; 366 [item setRepresentedObject:obj]; 367 // Finally, select the current parent. 368 NSValue* parentValue = [NSValue valueWithPointer:node_->parent()]; 369 NSInteger idx = [menu indexOfItemWithRepresentedObject:parentValue]; 370 [folderPopUpButton_ selectItemAtIndex:idx]; 371 } 372 373 @end // BookmarkBubbleController 374 375 376 @implementation BookmarkBubbleController(ExposedForUnitTesting) 377 378 + (NSString*)chooseAnotherFolderString { 379 return l10n_util::GetNSStringWithFixup( 380 IDS_BOOMARK_BUBBLE_CHOOSER_ANOTHER_FOLDER); 381 } 382 383 // For the given folder node, walk the tree and add folder names to 384 // the given pop up button. 385 - (void)addFolderNodes:(const BookmarkNode*)parent 386 toPopUpButton:(NSPopUpButton*)button 387 indentation:(int)indentation { 388 if (!model_->is_root(parent)) { 389 NSString* title = base::SysUTF16ToNSString(parent->GetTitle()); 390 NSMenu* menu = [button menu]; 391 NSMenuItem* item = [menu addItemWithTitle:title 392 action:NULL 393 keyEquivalent:@""]; 394 [item setRepresentedObject:[NSValue valueWithPointer:parent]]; 395 [item setIndentationLevel:indentation]; 396 ++indentation; 397 } 398 for (int i = 0; i < parent->child_count(); i++) { 399 const BookmarkNode* child = parent->GetChild(i); 400 if (child->is_folder()) 401 [self addFolderNodes:child 402 toPopUpButton:button 403 indentation:indentation]; 404 } 405 } 406 407 - (void)setTitle:(NSString*)title parentFolder:(const BookmarkNode*)parent { 408 [nameTextField_ setStringValue:title]; 409 [self setParentFolderSelection:parent]; 410 } 411 412 // Pick a specific parent node in the selection by finding the right 413 // pop up button index. 414 - (void)setParentFolderSelection:(const BookmarkNode*)parent { 415 // Expectation: There is a parent mapping for all items in the 416 // folderPopUpButton except the last one ("Choose another folder..."). 417 NSMenu* menu = [folderPopUpButton_ menu]; 418 NSValue* parentValue = [NSValue valueWithPointer:parent]; 419 NSInteger idx = [menu indexOfItemWithRepresentedObject:parentValue]; 420 DCHECK(idx != -1); 421 [folderPopUpButton_ selectItemAtIndex:idx]; 422 } 423 424 - (NSPopUpButton*)folderPopUpButton { 425 return folderPopUpButton_; 426 } 427 428 @end // implementation BookmarkBubbleController(ExposedForUnitTesting) 429