Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / bookmarks / bookmark_bubble_controller.mm
blobe1d41ee478d51bfa8f6bb7e0601ebb793987166d
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/strings/sys_string_conversions.h"
9 #include "chrome/browser/ui/bookmarks/bookmark_bubble_observer.h"
10 #include "chrome/browser/ui/browser.h"
11 #include "chrome/browser/ui/browser_finder.h"
12 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_button.h"
13 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_sync_promo_controller.h"
14 #import "chrome/browser/ui/cocoa/browser_window_controller.h"
15 #import "chrome/browser/ui/cocoa/info_bubble_view.h"
16 #include "chrome/browser/ui/sync/sync_promo_ui.h"
17 #include "chrome/grit/generated_resources.h"
18 #include "components/bookmarks/browser/bookmark_model.h"
19 #include "components/bookmarks/browser/bookmark_utils.h"
20 #include "components/bookmarks/managed/managed_bookmark_service.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;
28 using bookmarks::BookmarkModel;
29 using bookmarks::BookmarkNode;
31 // An object to represent the ChooseAnotherFolder item in the pop up.
32 @interface ChooseAnotherFolder : NSObject
33 @end
35 @implementation ChooseAnotherFolder
36 @end
38 @interface BookmarkBubbleController (PrivateAPI)
39 - (void)updateBookmarkNode;
40 - (void)fillInFolderList;
41 @end
43 @implementation BookmarkBubbleController
45 @synthesize node = node_;
47 + (id)chooseAnotherFolderObject {
48   // Singleton object to act as a representedObject for the "choose another
49   // folder" item in the pop up.
50   static ChooseAnotherFolder* object = nil;
51   if (!object) {
52     object = [[ChooseAnotherFolder alloc] init];
53   }
54   return object;
57 - (id)initWithParentWindow:(NSWindow*)parentWindow
58             bubbleObserver:(bookmarks::BookmarkBubbleObserver*)bubbleObserver
59                    managed:(bookmarks::ManagedBookmarkService*)managed
60                      model:(BookmarkModel*)model
61                       node:(const BookmarkNode*)node
62          alreadyBookmarked:(BOOL)alreadyBookmarked {
63   DCHECK(managed);
64   DCHECK(node);
65   if ((self = [super initWithWindowNibPath:@"BookmarkBubble"
66                               parentWindow:parentWindow
67                                 anchoredAt:NSZeroPoint])) {
68     bookmarkBubbleObserver_ = bubbleObserver;
69     managedBookmarkService_ = managed;
70     model_ = model;
71     node_ = node;
72     alreadyBookmarked_ = alreadyBookmarked;
73   }
74   return self;
77 - (void)awakeFromNib {
78   [super awakeFromNib];
80   [[nameTextField_ cell] setUsesSingleLineMode:YES];
82   Browser* browser = chrome::FindBrowserWithWindow(self.parentWindow);
83   if (SyncPromoUI::ShouldShowSyncPromo(browser->profile())) {
84     syncPromoController_.reset(
85         [[BookmarkSyncPromoController alloc] initWithBrowser:browser]);
86     [syncPromoPlaceholder_ addSubview:[syncPromoController_ view]];
88     // Resize the sync promo and its placeholder.
89     NSRect syncPromoPlaceholderFrame = [syncPromoPlaceholder_ frame];
90     CGFloat syncPromoHeight = [syncPromoController_
91         preferredHeightForWidth:syncPromoPlaceholderFrame.size.width];
92     syncPromoPlaceholderFrame.size.height = syncPromoHeight;
94     [syncPromoPlaceholder_ setFrame:syncPromoPlaceholderFrame];
95     [[syncPromoController_ view] setFrame:syncPromoPlaceholderFrame];
97     // Adjust the height of the bubble so that the sync promo fits in it,
98     // except for its bottom border. The xib file hides the left and right
99     // borders of the sync promo.
100     NSRect bubbleFrame = [[self window] frame];
101     bubbleFrame.size.height +=
102         syncPromoHeight - [syncPromoController_ borderWidth];
103     [[self window] setFrame:bubbleFrame display:YES];
104   }
107 - (void)notifyBubbleClosed {
108   if (!bookmarkBubbleObserver_)
109     return;
111   bookmarkBubbleObserver_->OnBookmarkBubbleHidden();
112   bookmarkBubbleObserver_ = nullptr;
115 // Close the bookmark bubble without changing anything.  Unlike a
116 // typical dialog's OK/Cancel, where Cancel is "do nothing", all
117 // buttons on the bubble have the capacity to change the bookmark
118 // model.  This is an IBOutlet-looking entry point to remove the
119 // dialog without touching the model.
120 - (void)dismissWithoutEditing:(id)sender {
121   [self close];
124 - (void)windowWillClose:(NSNotification*)notification {
125   // We caught a close so we don't need to watch for the parent closing.
126   bookmarkObserver_.reset();
127   [self notifyBubbleClosed];
128   [super windowWillClose:notification];
131 // Override -[BaseBubbleController showWindow:] to tweak bubble location and
132 // set up UI elements.
133 - (void)showWindow:(id)sender {
134   NSWindow* window = [self window];  // Force load the NIB.
135   NSWindow* parentWindow = self.parentWindow;
136   BrowserWindowController* bwc =
137       [BrowserWindowController browserWindowControllerForWindow:parentWindow];
139   InfoBubbleView* bubble = self.bubble;
140   [bubble setArrowLocation:info_bubble::kTopRight];
142   // Insure decent positioning even in the absence of a browser controller,
143   // which will occur for some unit tests.
144   NSPoint arrowTip = bwc ? [bwc bookmarkBubblePoint] :
145       NSMakePoint([window frame].size.width, [window frame].size.height);
146   arrowTip = [parentWindow convertBaseToScreen:arrowTip];
147   NSPoint bubbleArrowTip = [bubble arrowTip];
148   bubbleArrowTip = [bubble convertPoint:bubbleArrowTip toView:nil];
149   arrowTip.y -= bubbleArrowTip.y;
150   arrowTip.x -= bubbleArrowTip.x;
151   [window setFrameOrigin:arrowTip];
153   // Default is IDS_BOOKMARK_BUBBLE_PAGE_BOOKMARK; "Bookmark".
154   // If adding for the 1st time the string becomes "Bookmark Added!"
155   if (!alreadyBookmarked_) {
156     NSString* title =
157         l10n_util::GetNSString(IDS_BOOKMARK_BUBBLE_PAGE_BOOKMARKED);
158     [bigTitle_ setStringValue:title];
159   }
161   [self fillInFolderList];
163   // Ping me when things change out from under us.  Unlike a normal
164   // dialog, the bookmark bubble's cancel: means "don't add this as a
165   // bookmark", not "cancel editing".  We must take extra care to not
166   // touch the bookmark in this selector.
167   bookmarkObserver_.reset(new BookmarkModelObserverForCocoa(model_, ^() {
168     [self dismissWithoutEditing:nil];
169   }));
170   bookmarkObserver_->StartObservingNode(node_);
172   [parentWindow addChildWindow:window ordered:NSWindowAbove];
173   [window makeKeyAndOrderFront:self];
174   [self registerKeyStateEventTap];
176   bookmarkBubbleObserver_->OnBookmarkBubbleShown(node_);
179 - (void)close {
180   [[BrowserWindowController browserWindowControllerForWindow:self.parentWindow]
181       releaseBarVisibilityForOwner:self withAnimation:YES delay:NO];
183   [super close];
186 // Shows the bookmark editor sheet for more advanced editing.
187 - (void)showEditor {
188   [self ok:self];
189   // Send the action up through the responder chain.
190   [NSApp sendAction:@selector(editBookmarkNode:) to:nil from:self];
193 - (IBAction)edit:(id)sender {
194   content::RecordAction(UserMetricsAction("BookmarkBubble_Edit"));
195   [self showEditor];
198 - (IBAction)ok:(id)sender {
199   [self updateBookmarkNode];
200   [self close];
203 // By implementing this, ESC causes the window to go away. If clicking the
204 // star was what prompted this bubble to appear (i.e., not already bookmarked),
205 // remove the bookmark.
206 - (IBAction)cancel:(id)sender {
207   if (!alreadyBookmarked_) {
208     // |-remove:| calls |-close| so don't do it.
209     [self remove:sender];
210   } else {
211     [self dismissWithoutEditing:nil];
212   }
215 - (IBAction)remove:(id)sender {
216   bookmarks::RemoveAllBookmarks(model_, node_->url());
217   content::RecordAction(UserMetricsAction("BookmarkBubble_Unstar"));
218   node_ = NULL;  // no longer valid
219   [self ok:sender];
222 // The controller is  the target of the pop up button box action so it can
223 // handle when "choose another folder" was picked.
224 - (IBAction)folderChanged:(id)sender {
225   DCHECK([sender isEqual:folderPopUpButton_]);
226   // It is possible that due to model change our parent window has been closed
227   // but the popup is still showing and able to notify the controller of a
228   // folder change.  We ignore the sender in this case.
229   if (!self.parentWindow)
230     return;
231   NSMenuItem* selected = [folderPopUpButton_ selectedItem];
232   ChooseAnotherFolder* chooseItem = [[self class] chooseAnotherFolderObject];
233   if ([[selected representedObject] isEqual:chooseItem]) {
234     content::RecordAction(
235         UserMetricsAction("BookmarkBubble_EditFromCombobox"));
236     [self showEditor];
237   }
240 // The controller is the delegate of the window so it receives did resign key
241 // notifications. When key is resigned mirror Windows behavior and close the
242 // window.
243 - (void)windowDidResignKey:(NSNotification*)notification {
244   NSWindow* window = [self window];
245   DCHECK_EQ([notification object], window);
246   if ([window isVisible]) {
247     // If the window isn't visible, it is already closed, and this notification
248     // has been sent as part of the closing operation, so no need to close.
249     [self ok:self];
250   }
253 // Look at the dialog; if the user has changed anything, update the
254 // bookmark node to reflect this.
255 - (void)updateBookmarkNode {
256   if (!node_) return;
258   // First the title...
259   NSString* oldTitle = base::SysUTF16ToNSString(node_->GetTitle());
260   NSString* newTitle = [nameTextField_ stringValue];
261   if (![oldTitle isEqual:newTitle]) {
262     model_->SetTitle(node_, base::SysNSStringToUTF16(newTitle));
263     content::RecordAction(
264         UserMetricsAction("BookmarkBubble_ChangeTitleInBubble"));
265   }
266   // Then the parent folder.
267   const BookmarkNode* oldParent = node_->parent();
268   NSMenuItem* selectedItem = [folderPopUpButton_ selectedItem];
269   id representedObject = [selectedItem representedObject];
270   if ([representedObject isEqual:[[self class] chooseAnotherFolderObject]]) {
271     // "Choose another folder..."
272     return;
273   }
274   const BookmarkNode* newParent =
275       static_cast<const BookmarkNode*>([representedObject pointerValue]);
276   DCHECK(newParent);
277   if (oldParent != newParent) {
278     int index = newParent->child_count();
279     model_->Move(node_, newParent, index);
280     content::RecordAction(UserMetricsAction("BookmarkBubble_ChangeParent"));
281   }
284 // Fill in all information related to the folder pop up button.
285 - (void)fillInFolderList {
286   [nameTextField_ setStringValue:base::SysUTF16ToNSString(node_->GetTitle())];
287   DCHECK([folderPopUpButton_ numberOfItems] == 0);
288   [self addFolderNodes:model_->root_node()
289          toPopUpButton:folderPopUpButton_
290            indentation:0];
291   NSMenu* menu = [folderPopUpButton_ menu];
292   [menu addItem:[NSMenuItem separatorItem]];
293   NSString* title = [[self class] chooseAnotherFolderString];
294   NSMenuItem *item = [menu addItemWithTitle:title
295                                      action:NULL
296                               keyEquivalent:@""];
297   ChooseAnotherFolder* obj = [[self class] chooseAnotherFolderObject];
298   [item setRepresentedObject:obj];
299   // Finally, select the current parent.
300   NSValue* parentValue = [NSValue valueWithPointer:node_->parent()];
301   NSInteger idx = [menu indexOfItemWithRepresentedObject:parentValue];
302   [folderPopUpButton_ selectItemAtIndex:idx];
305 @end  // BookmarkBubbleController
308 @implementation BookmarkBubbleController (ExposedForUnitTesting)
310 - (NSView*)syncPromoPlaceholder {
311   return syncPromoPlaceholder_;
314 - (bookmarks::BookmarkBubbleObserver*)bookmarkBubbleObserver {
315   return bookmarkBubbleObserver_;
318 + (NSString*)chooseAnotherFolderString {
319   return l10n_util::GetNSStringWithFixup(
320       IDS_BOOKMARK_BUBBLE_CHOOSER_ANOTHER_FOLDER);
323 // For the given folder node, walk the tree and add folder names to
324 // the given pop up button.
325 - (void)addFolderNodes:(const BookmarkNode*)parent
326          toPopUpButton:(NSPopUpButton*)button
327            indentation:(int)indentation {
328   if (!model_->is_root_node(parent)) {
329     NSString* title = base::SysUTF16ToNSString(parent->GetTitle());
330     NSMenu* menu = [button menu];
331     NSMenuItem* item = [menu addItemWithTitle:title
332                                        action:NULL
333                                 keyEquivalent:@""];
334     [item setRepresentedObject:[NSValue valueWithPointer:parent]];
335     [item setIndentationLevel:indentation];
336     ++indentation;
337   }
338   for (int i = 0; i < parent->child_count(); i++) {
339     const BookmarkNode* child = parent->GetChild(i);
340     if (child->is_folder() && child->IsVisible() &&
341         managedBookmarkService_->CanBeEditedByUser(child)) {
342       [self addFolderNodes:child
343              toPopUpButton:button
344                indentation:indentation];
345     }
346   }
349 - (void)setTitle:(NSString*)title parentFolder:(const BookmarkNode*)parent {
350   [nameTextField_ setStringValue:title];
351   [self setParentFolderSelection:parent];
354 // Pick a specific parent node in the selection by finding the right
355 // pop up button index.
356 - (void)setParentFolderSelection:(const BookmarkNode*)parent {
357   // Expectation: There is a parent mapping for all items in the
358   // folderPopUpButton except the last one ("Choose another folder...").
359   NSMenu* menu = [folderPopUpButton_ menu];
360   NSValue* parentValue = [NSValue valueWithPointer:parent];
361   NSInteger idx = [menu indexOfItemWithRepresentedObject:parentValue];
362   DCHECK(idx != -1);
363   [folderPopUpButton_ selectItemAtIndex:idx];
366 - (NSPopUpButton*)folderPopUpButton {
367   return folderPopUpButton_;
370 @end  // implementation BookmarkBubbleController(ExposedForUnitTesting)