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. 24class 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 38BookmarkBubbleNotificationBridge::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. 51void 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