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.
5 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_bubble_controller.h"
7 #include "base/mac/bundle_locations.h"
8 #include "base/mac/mac_util.h"
9 #include "base/strings/sys_string_conversions.h"
10 #include "chrome/browser/bookmarks/chrome_bookmark_client.h"
11 #include "chrome/browser/ui/browser.h"
12 #include "chrome/browser/ui/browser_finder.h"
13 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_button.h"
14 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_sync_promo_controller.h"
15 #import "chrome/browser/ui/cocoa/browser_window_controller.h"
16 #import "chrome/browser/ui/cocoa/info_bubble_view.h"
17 #include "chrome/browser/ui/sync/sync_promo_ui.h"
18 #include "chrome/grit/generated_resources.h"
19 #include "components/bookmarks/browser/bookmark_model.h"
20 #include "components/bookmarks/browser/bookmark_utils.h"
21 #include "content/public/browser/notification_observer.h"
22 #include "content/public/browser/notification_registrar.h"
23 #include "content/public/browser/notification_service.h"
24 #include "content/public/browser/user_metrics.h"
25 #include "ui/base/l10n/l10n_util_mac.h"
27 using base::UserMetricsAction;
29 // An object to represent the ChooseAnotherFolder item in the pop up.
30 @interface ChooseAnotherFolder : NSObject
33 @implementation ChooseAnotherFolder
36 @interface BookmarkBubbleController (PrivateAPI)
37 - (void)updateBookmarkNode;
38 - (void)fillInFolderList;
41 @implementation BookmarkBubbleController
43 @synthesize node = node_;
45 + (id)chooseAnotherFolderObject {
46 // Singleton object to act as a representedObject for the "choose another
47 // folder" item in the pop up.
48 static ChooseAnotherFolder* object = nil;
50 object = [[ChooseAnotherFolder alloc] init];
55 - (id)initWithParentWindow:(NSWindow*)parentWindow
56 client:(ChromeBookmarkClient*)client
57 model:(BookmarkModel*)model
58 node:(const BookmarkNode*)node
59 alreadyBookmarked:(BOOL)alreadyBookmarked {
62 if ((self = [super initWithWindowNibPath:@"BookmarkBubble"
63 parentWindow:parentWindow
64 anchoredAt:NSZeroPoint])) {
68 alreadyBookmarked_ = alreadyBookmarked;
73 - (void)awakeFromNib {
76 [[nameTextField_ cell] setUsesSingleLineMode:YES];
78 Browser* browser = chrome::FindBrowserWithWindow(self.parentWindow);
79 if (SyncPromoUI::ShouldShowSyncPromo(browser->profile())) {
80 syncPromoController_.reset(
81 [[BookmarkSyncPromoController alloc] initWithBrowser:browser]);
82 [syncPromoPlaceholder_ addSubview:[syncPromoController_ view]];
84 // Resize the sync promo and its placeholder.
85 NSRect syncPromoPlaceholderFrame = [syncPromoPlaceholder_ frame];
86 CGFloat syncPromoHeight = [syncPromoController_
87 preferredHeightForWidth:syncPromoPlaceholderFrame.size.width];
88 syncPromoPlaceholderFrame.size.height = syncPromoHeight;
90 [syncPromoPlaceholder_ setFrame:syncPromoPlaceholderFrame];
91 [[syncPromoController_ view] setFrame:syncPromoPlaceholderFrame];
93 // Adjust the height of the bubble so that the sync promo fits in it,
94 // except for its bottom border. The xib file hides the left and right
95 // borders of the sync promo.
96 NSRect bubbleFrame = [[self window] frame];
97 bubbleFrame.size.height +=
98 syncPromoHeight - [syncPromoController_ borderWidth];
99 [[self window] setFrame:bubbleFrame display:YES];
103 // If this is a new bookmark somewhere visible (e.g. on the bookmark
104 // bar), pulse it. Else, call ourself recursively with our parent
105 // until we find something visible to pulse.
106 - (void)startPulsingBookmarkButton:(const BookmarkNode*)node {
108 if ((node->parent() == model_->bookmark_bar_node()) ||
109 (node->parent() == client_->managed_node()) ||
110 (node == model_->other_node())) {
111 pulsingBookmarkNode_ = node;
112 bookmarkObserver_->StartObservingNode(pulsingBookmarkNode_);
113 NSValue *value = [NSValue valueWithPointer:node];
114 NSDictionary *dict = [NSDictionary
115 dictionaryWithObjectsAndKeys:value,
116 bookmark_button::kBookmarkKey,
117 [NSNumber numberWithBool:YES],
118 bookmark_button::kBookmarkPulseFlagKey,
120 [[NSNotificationCenter defaultCenter]
121 postNotificationName:bookmark_button::kPulseBookmarkButtonNotification
126 node = node->parent();
130 - (void)stopPulsingBookmarkButton {
131 if (!pulsingBookmarkNode_)
133 NSValue *value = [NSValue valueWithPointer:pulsingBookmarkNode_];
134 if (bookmarkObserver_)
135 bookmarkObserver_->StopObservingNode(pulsingBookmarkNode_);
136 pulsingBookmarkNode_ = NULL;
137 NSDictionary *dict = [NSDictionary
138 dictionaryWithObjectsAndKeys:value,
139 bookmark_button::kBookmarkKey,
140 [NSNumber numberWithBool:NO],
141 bookmark_button::kBookmarkPulseFlagKey,
143 [[NSNotificationCenter defaultCenter]
144 postNotificationName:bookmark_button::kPulseBookmarkButtonNotification
149 // Close the bookmark bubble without changing anything. Unlike a
150 // typical dialog's OK/Cancel, where Cancel is "do nothing", all
151 // buttons on the bubble have the capacity to change the bookmark
152 // model. This is an IBOutlet-looking entry point to remove the
153 // dialog without touching the model.
154 - (void)dismissWithoutEditing:(id)sender {
158 - (void)windowWillClose:(NSNotification*)notification {
159 // We caught a close so we don't need to watch for the parent closing.
160 bookmarkObserver_.reset();
161 [self stopPulsingBookmarkButton];
162 [super windowWillClose:notification];
165 // Override -[BaseBubbleController showWindow:] to tweak bubble location and
166 // set up UI elements.
167 - (void)showWindow:(id)sender {
168 NSWindow* window = [self window]; // Force load the NIB.
169 NSWindow* parentWindow = self.parentWindow;
170 BrowserWindowController* bwc =
171 [BrowserWindowController browserWindowControllerForWindow:parentWindow];
172 [bwc lockBarVisibilityForOwner:self withAnimation:NO delay:NO];
174 InfoBubbleView* bubble = self.bubble;
175 [bubble setArrowLocation:info_bubble::kTopRight];
177 // Insure decent positioning even in the absence of a browser controller,
178 // which will occur for some unit tests.
179 NSPoint arrowTip = bwc ? [bwc bookmarkBubblePoint] :
180 NSMakePoint([window frame].size.width, [window frame].size.height);
181 arrowTip = [parentWindow convertBaseToScreen:arrowTip];
182 NSPoint bubbleArrowTip = [bubble arrowTip];
183 bubbleArrowTip = [bubble convertPoint:bubbleArrowTip toView:nil];
184 arrowTip.y -= bubbleArrowTip.y;
185 arrowTip.x -= bubbleArrowTip.x;
186 [window setFrameOrigin:arrowTip];
188 // Default is IDS_BOOKMARK_BUBBLE_PAGE_BOOKMARK; "Bookmark".
189 // If adding for the 1st time the string becomes "Bookmark Added!"
190 if (!alreadyBookmarked_) {
192 l10n_util::GetNSString(IDS_BOOKMARK_BUBBLE_PAGE_BOOKMARKED);
193 [bigTitle_ setStringValue:title];
196 [self fillInFolderList];
198 // Ping me when things change out from under us. Unlike a normal
199 // dialog, the bookmark bubble's cancel: means "don't add this as a
200 // bookmark", not "cancel editing". We must take extra care to not
201 // touch the bookmark in this selector.
202 bookmarkObserver_.reset(
203 new BookmarkModelObserverForCocoa(model_, ^(BOOL nodeWasDeleted) {
204 // If a watched node was deleted, the pointer to the pulsing button
207 pulsingBookmarkNode_ = NULL;
208 [self dismissWithoutEditing:nil];
210 bookmarkObserver_->StartObservingNode(node_);
212 // Pulse something interesting on the bookmark bar.
213 [self startPulsingBookmarkButton:node_];
215 [parentWindow addChildWindow:window ordered:NSWindowAbove];
216 [window makeKeyAndOrderFront:self];
217 [self registerKeyStateEventTap];
221 [[BrowserWindowController browserWindowControllerForWindow:self.parentWindow]
222 releaseBarVisibilityForOwner:self withAnimation:YES delay:NO];
227 // Shows the bookmark editor sheet for more advanced editing.
230 // Send the action up through the responder chain.
231 [NSApp sendAction:@selector(editBookmarkNode:) to:nil from:self];
234 - (IBAction)edit:(id)sender {
235 content::RecordAction(UserMetricsAction("BookmarkBubble_Edit"));
239 - (IBAction)ok:(id)sender {
240 [self stopPulsingBookmarkButton]; // before parent changes
241 [self updateBookmarkNode];
245 // By implementing this, ESC causes the window to go away. If clicking the
246 // star was what prompted this bubble to appear (i.e., not already bookmarked),
247 // remove the bookmark.
248 - (IBAction)cancel:(id)sender {
249 if (!alreadyBookmarked_) {
250 // |-remove:| calls |-close| so don't do it.
251 [self remove:sender];
253 [self stopPulsingBookmarkButton];
254 [self dismissWithoutEditing:nil];
258 - (IBAction)remove:(id)sender {
259 [self stopPulsingBookmarkButton];
260 bookmarks::RemoveAllBookmarks(model_, node_->url());
261 content::RecordAction(UserMetricsAction("BookmarkBubble_Unstar"));
262 node_ = NULL; // no longer valid
266 // The controller is the target of the pop up button box action so it can
267 // handle when "choose another folder" was picked.
268 - (IBAction)folderChanged:(id)sender {
269 DCHECK([sender isEqual:folderPopUpButton_]);
270 // It is possible that due to model change our parent window has been closed
271 // but the popup is still showing and able to notify the controller of a
272 // folder change. We ignore the sender in this case.
273 if (!self.parentWindow)
275 NSMenuItem* selected = [folderPopUpButton_ selectedItem];
276 ChooseAnotherFolder* chooseItem = [[self class] chooseAnotherFolderObject];
277 if ([[selected representedObject] isEqual:chooseItem]) {
278 content::RecordAction(
279 UserMetricsAction("BookmarkBubble_EditFromCombobox"));
284 // The controller is the delegate of the window so it receives did resign key
285 // notifications. When key is resigned mirror Windows behavior and close the
287 - (void)windowDidResignKey:(NSNotification*)notification {
288 NSWindow* window = [self window];
289 DCHECK_EQ([notification object], window);
290 if ([window isVisible]) {
291 // If the window isn't visible, it is already closed, and this notification
292 // has been sent as part of the closing operation, so no need to close.
297 // Look at the dialog; if the user has changed anything, update the
298 // bookmark node to reflect this.
299 - (void)updateBookmarkNode {
302 // First the title...
303 NSString* oldTitle = base::SysUTF16ToNSString(node_->GetTitle());
304 NSString* newTitle = [nameTextField_ stringValue];
305 if (![oldTitle isEqual:newTitle]) {
306 model_->SetTitle(node_, base::SysNSStringToUTF16(newTitle));
307 content::RecordAction(
308 UserMetricsAction("BookmarkBubble_ChangeTitleInBubble"));
310 // Then the parent folder.
311 const BookmarkNode* oldParent = node_->parent();
312 NSMenuItem* selectedItem = [folderPopUpButton_ selectedItem];
313 id representedObject = [selectedItem representedObject];
314 if ([representedObject isEqual:[[self class] chooseAnotherFolderObject]]) {
315 // "Choose another folder..."
318 const BookmarkNode* newParent =
319 static_cast<const BookmarkNode*>([representedObject pointerValue]);
321 if (oldParent != newParent) {
322 int index = newParent->child_count();
323 model_->Move(node_, newParent, index);
324 content::RecordAction(UserMetricsAction("BookmarkBubble_ChangeParent"));
328 // Fill in all information related to the folder pop up button.
329 - (void)fillInFolderList {
330 [nameTextField_ setStringValue:base::SysUTF16ToNSString(node_->GetTitle())];
331 DCHECK([folderPopUpButton_ numberOfItems] == 0);
332 [self addFolderNodes:model_->root_node()
333 toPopUpButton:folderPopUpButton_
335 NSMenu* menu = [folderPopUpButton_ menu];
336 [menu addItem:[NSMenuItem separatorItem]];
337 NSString* title = [[self class] chooseAnotherFolderString];
338 NSMenuItem *item = [menu addItemWithTitle:title
341 ChooseAnotherFolder* obj = [[self class] chooseAnotherFolderObject];
342 [item setRepresentedObject:obj];
343 // Finally, select the current parent.
344 NSValue* parentValue = [NSValue valueWithPointer:node_->parent()];
345 NSInteger idx = [menu indexOfItemWithRepresentedObject:parentValue];
346 [folderPopUpButton_ selectItemAtIndex:idx];
349 @end // BookmarkBubbleController
352 @implementation BookmarkBubbleController (ExposedForUnitTesting)
354 - (NSView*)syncPromoPlaceholder {
355 return syncPromoPlaceholder_;
358 + (NSString*)chooseAnotherFolderString {
359 return l10n_util::GetNSStringWithFixup(
360 IDS_BOOKMARK_BUBBLE_CHOOSER_ANOTHER_FOLDER);
363 // For the given folder node, walk the tree and add folder names to
364 // the given pop up button.
365 - (void)addFolderNodes:(const BookmarkNode*)parent
366 toPopUpButton:(NSPopUpButton*)button
367 indentation:(int)indentation {
368 if (!model_->is_root_node(parent)) {
369 NSString* title = base::SysUTF16ToNSString(parent->GetTitle());
370 NSMenu* menu = [button menu];
371 NSMenuItem* item = [menu addItemWithTitle:title
374 [item setRepresentedObject:[NSValue valueWithPointer:parent]];
375 [item setIndentationLevel:indentation];
378 for (int i = 0; i < parent->child_count(); i++) {
379 const BookmarkNode* child = parent->GetChild(i);
380 if (child->is_folder() && child->IsVisible() &&
381 client_->CanBeEditedByUser(child)) {
382 [self addFolderNodes:child
384 indentation:indentation];
389 - (void)setTitle:(NSString*)title parentFolder:(const BookmarkNode*)parent {
390 [nameTextField_ setStringValue:title];
391 [self setParentFolderSelection:parent];
394 // Pick a specific parent node in the selection by finding the right
395 // pop up button index.
396 - (void)setParentFolderSelection:(const BookmarkNode*)parent {
397 // Expectation: There is a parent mapping for all items in the
398 // folderPopUpButton except the last one ("Choose another folder...").
399 NSMenu* menu = [folderPopUpButton_ menu];
400 NSValue* parentValue = [NSValue valueWithPointer:parent];
401 NSInteger idx = [menu indexOfItemWithRepresentedObject:parentValue];
403 [folderPopUpButton_ selectItemAtIndex:idx];
406 - (NSPopUpButton*)folderPopUpButton {
407 return folderPopUpButton_;
410 @end // implementation BookmarkBubbleController(ExposedForUnitTesting)