Revert of Add button to add new FSP services to Files app. (patchset #8 id:140001...
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / bookmarks / bookmark_bubble_controller.mm
blob59999b16f16928dc62a68318b66ae6c5030a410a
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/bookmarks/chrome_bookmark_client.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 "content/public/browser/notification_observer.h"
21 #include "content/public/browser/notification_registrar.h"
22 #include "content/public/browser/notification_service.h"
23 #include "content/public/browser/user_metrics.h"
24 #include "ui/base/l10n/l10n_util_mac.h"
26 using base::UserMetricsAction;
27 using bookmarks::BookmarkModel;
28 using bookmarks::BookmarkNode;
30 // An object to represent the ChooseAnotherFolder item in the pop up.
31 @interface ChooseAnotherFolder : NSObject
32 @end
34 @implementation ChooseAnotherFolder
35 @end
37 @interface BookmarkBubbleController (PrivateAPI)
38 - (void)updateBookmarkNode;
39 - (void)fillInFolderList;
40 @end
42 @implementation BookmarkBubbleController
44 @synthesize node = node_;
46 + (id)chooseAnotherFolderObject {
47   // Singleton object to act as a representedObject for the "choose another
48   // folder" item in the pop up.
49   static ChooseAnotherFolder* object = nil;
50   if (!object) {
51     object = [[ChooseAnotherFolder alloc] init];
52   }
53   return object;
56 - (id)initWithParentWindow:(NSWindow*)parentWindow
57                     client:(ChromeBookmarkClient*)client
58                      model:(BookmarkModel*)model
59                       node:(const BookmarkNode*)node
60          alreadyBookmarked:(BOOL)alreadyBookmarked {
61   DCHECK(client);
62   DCHECK(node);
63   if ((self = [super initWithWindowNibPath:@"BookmarkBubble"
64                               parentWindow:parentWindow
65                                 anchoredAt:NSZeroPoint])) {
66     client_ = client;
67     model_ = model;
68     node_ = node;
69     alreadyBookmarked_ = alreadyBookmarked;
70   }
71   return self;
74 - (void)awakeFromNib {
75   [super awakeFromNib];
77   [[nameTextField_ cell] setUsesSingleLineMode:YES];
79   Browser* browser = chrome::FindBrowserWithWindow(self.parentWindow);
80   if (SyncPromoUI::ShouldShowSyncPromo(browser->profile())) {
81     syncPromoController_.reset(
82         [[BookmarkSyncPromoController alloc] initWithBrowser:browser]);
83     [syncPromoPlaceholder_ addSubview:[syncPromoController_ view]];
85     // Resize the sync promo and its placeholder.
86     NSRect syncPromoPlaceholderFrame = [syncPromoPlaceholder_ frame];
87     CGFloat syncPromoHeight = [syncPromoController_
88         preferredHeightForWidth:syncPromoPlaceholderFrame.size.width];
89     syncPromoPlaceholderFrame.size.height = syncPromoHeight;
91     [syncPromoPlaceholder_ setFrame:syncPromoPlaceholderFrame];
92     [[syncPromoController_ view] setFrame:syncPromoPlaceholderFrame];
94     // Adjust the height of the bubble so that the sync promo fits in it,
95     // except for its bottom border. The xib file hides the left and right
96     // borders of the sync promo.
97     NSRect bubbleFrame = [[self window] frame];
98     bubbleFrame.size.height +=
99         syncPromoHeight - [syncPromoController_ borderWidth];
100     [[self window] setFrame:bubbleFrame display:YES];
101   }
104 // If this is a new bookmark somewhere visible (e.g. on the bookmark
105 // bar), pulse it.  Else, call ourself recursively with our parent
106 // until we find something visible to pulse.
107 - (void)startPulsingBookmarkButton:(const BookmarkNode*)node  {
108   while (node) {
109     if ((node->parent() == model_->bookmark_bar_node()) ||
110         (node->parent() == client_->managed_node()) ||
111         (node->parent() == client_->supervised_node()) ||
112         (node == model_->other_node())) {
113       pulsingBookmarkNode_ = node;
114       bookmarkObserver_->StartObservingNode(pulsingBookmarkNode_);
115       NSValue *value = [NSValue valueWithPointer:node];
116       NSDictionary *dict = [NSDictionary
117                              dictionaryWithObjectsAndKeys:value,
118                              bookmark_button::kBookmarkKey,
119                              [NSNumber numberWithBool:YES],
120                              bookmark_button::kBookmarkPulseFlagKey,
121                              nil];
122       [[NSNotificationCenter defaultCenter]
123         postNotificationName:bookmark_button::kPulseBookmarkButtonNotification
124                       object:self
125                     userInfo:dict];
126       return;
127     }
128     node = node->parent();
129   }
132 - (void)stopPulsingBookmarkButton {
133   if (!pulsingBookmarkNode_)
134     return;
135   NSValue *value = [NSValue valueWithPointer:pulsingBookmarkNode_];
136   if (bookmarkObserver_)
137       bookmarkObserver_->StopObservingNode(pulsingBookmarkNode_);
138   pulsingBookmarkNode_ = NULL;
139   NSDictionary *dict = [NSDictionary
140                          dictionaryWithObjectsAndKeys:value,
141                          bookmark_button::kBookmarkKey,
142                          [NSNumber numberWithBool:NO],
143                          bookmark_button::kBookmarkPulseFlagKey,
144                          nil];
145   [[NSNotificationCenter defaultCenter]
146         postNotificationName:bookmark_button::kPulseBookmarkButtonNotification
147                       object:self
148                     userInfo:dict];
151 // Close the bookmark bubble without changing anything.  Unlike a
152 // typical dialog's OK/Cancel, where Cancel is "do nothing", all
153 // buttons on the bubble have the capacity to change the bookmark
154 // model.  This is an IBOutlet-looking entry point to remove the
155 // dialog without touching the model.
156 - (void)dismissWithoutEditing:(id)sender {
157   [self close];
160 - (void)windowWillClose:(NSNotification*)notification {
161   // We caught a close so we don't need to watch for the parent closing.
162   bookmarkObserver_.reset();
163   [self stopPulsingBookmarkButton];
164   [super windowWillClose:notification];
167 // Override -[BaseBubbleController showWindow:] to tweak bubble location and
168 // set up UI elements.
169 - (void)showWindow:(id)sender {
170   NSWindow* window = [self window];  // Force load the NIB.
171   NSWindow* parentWindow = self.parentWindow;
172   BrowserWindowController* bwc =
173       [BrowserWindowController browserWindowControllerForWindow:parentWindow];
174   [bwc lockBarVisibilityForOwner:self withAnimation:NO delay:NO];
176   InfoBubbleView* bubble = self.bubble;
177   [bubble setArrowLocation:info_bubble::kTopRight];
179   // Insure decent positioning even in the absence of a browser controller,
180   // which will occur for some unit tests.
181   NSPoint arrowTip = bwc ? [bwc bookmarkBubblePoint] :
182       NSMakePoint([window frame].size.width, [window frame].size.height);
183   arrowTip = [parentWindow convertBaseToScreen:arrowTip];
184   NSPoint bubbleArrowTip = [bubble arrowTip];
185   bubbleArrowTip = [bubble convertPoint:bubbleArrowTip toView:nil];
186   arrowTip.y -= bubbleArrowTip.y;
187   arrowTip.x -= bubbleArrowTip.x;
188   [window setFrameOrigin:arrowTip];
190   // Default is IDS_BOOKMARK_BUBBLE_PAGE_BOOKMARK; "Bookmark".
191   // If adding for the 1st time the string becomes "Bookmark Added!"
192   if (!alreadyBookmarked_) {
193     NSString* title =
194         l10n_util::GetNSString(IDS_BOOKMARK_BUBBLE_PAGE_BOOKMARKED);
195     [bigTitle_ setStringValue:title];
196   }
198   [self fillInFolderList];
200   // Ping me when things change out from under us.  Unlike a normal
201   // dialog, the bookmark bubble's cancel: means "don't add this as a
202   // bookmark", not "cancel editing".  We must take extra care to not
203   // touch the bookmark in this selector.
204   bookmarkObserver_.reset(
205       new BookmarkModelObserverForCocoa(model_, ^(BOOL nodeWasDeleted) {
206           // If a watched node was deleted, the pointer to the pulsing button
207           // is likely stale.
208           if (nodeWasDeleted)
209             pulsingBookmarkNode_ = NULL;
210           [self dismissWithoutEditing:nil];
211       }));
212   bookmarkObserver_->StartObservingNode(node_);
214   // Pulse something interesting on the bookmark bar.
215   [self startPulsingBookmarkButton:node_];
217   [parentWindow addChildWindow:window ordered:NSWindowAbove];
218   [window makeKeyAndOrderFront:self];
219   [self registerKeyStateEventTap];
222 - (void)close {
223   [[BrowserWindowController browserWindowControllerForWindow:self.parentWindow]
224       releaseBarVisibilityForOwner:self withAnimation:YES delay:NO];
226   [super close];
229 // Shows the bookmark editor sheet for more advanced editing.
230 - (void)showEditor {
231   [self ok:self];
232   // Send the action up through the responder chain.
233   [NSApp sendAction:@selector(editBookmarkNode:) to:nil from:self];
236 - (IBAction)edit:(id)sender {
237   content::RecordAction(UserMetricsAction("BookmarkBubble_Edit"));
238   [self showEditor];
241 - (IBAction)ok:(id)sender {
242   [self stopPulsingBookmarkButton];  // before parent changes
243   [self updateBookmarkNode];
244   [self close];
247 // By implementing this, ESC causes the window to go away. If clicking the
248 // star was what prompted this bubble to appear (i.e., not already bookmarked),
249 // remove the bookmark.
250 - (IBAction)cancel:(id)sender {
251   if (!alreadyBookmarked_) {
252     // |-remove:| calls |-close| so don't do it.
253     [self remove:sender];
254   } else {
255     [self stopPulsingBookmarkButton];
256     [self dismissWithoutEditing:nil];
257   }
260 - (IBAction)remove:(id)sender {
261   [self stopPulsingBookmarkButton];
262   bookmarks::RemoveAllBookmarks(model_, node_->url());
263   content::RecordAction(UserMetricsAction("BookmarkBubble_Unstar"));
264   node_ = NULL;  // no longer valid
265   [self ok:sender];
268 // The controller is  the target of the pop up button box action so it can
269 // handle when "choose another folder" was picked.
270 - (IBAction)folderChanged:(id)sender {
271   DCHECK([sender isEqual:folderPopUpButton_]);
272   // It is possible that due to model change our parent window has been closed
273   // but the popup is still showing and able to notify the controller of a
274   // folder change.  We ignore the sender in this case.
275   if (!self.parentWindow)
276     return;
277   NSMenuItem* selected = [folderPopUpButton_ selectedItem];
278   ChooseAnotherFolder* chooseItem = [[self class] chooseAnotherFolderObject];
279   if ([[selected representedObject] isEqual:chooseItem]) {
280     content::RecordAction(
281         UserMetricsAction("BookmarkBubble_EditFromCombobox"));
282     [self showEditor];
283   }
286 // The controller is the delegate of the window so it receives did resign key
287 // notifications. When key is resigned mirror Windows behavior and close the
288 // window.
289 - (void)windowDidResignKey:(NSNotification*)notification {
290   NSWindow* window = [self window];
291   DCHECK_EQ([notification object], window);
292   if ([window isVisible]) {
293     // If the window isn't visible, it is already closed, and this notification
294     // has been sent as part of the closing operation, so no need to close.
295     [self ok:self];
296   }
299 // Look at the dialog; if the user has changed anything, update the
300 // bookmark node to reflect this.
301 - (void)updateBookmarkNode {
302   if (!node_) return;
304   // First the title...
305   NSString* oldTitle = base::SysUTF16ToNSString(node_->GetTitle());
306   NSString* newTitle = [nameTextField_ stringValue];
307   if (![oldTitle isEqual:newTitle]) {
308     model_->SetTitle(node_, base::SysNSStringToUTF16(newTitle));
309     content::RecordAction(
310         UserMetricsAction("BookmarkBubble_ChangeTitleInBubble"));
311   }
312   // Then the parent folder.
313   const BookmarkNode* oldParent = node_->parent();
314   NSMenuItem* selectedItem = [folderPopUpButton_ selectedItem];
315   id representedObject = [selectedItem representedObject];
316   if ([representedObject isEqual:[[self class] chooseAnotherFolderObject]]) {
317     // "Choose another folder..."
318     return;
319   }
320   const BookmarkNode* newParent =
321       static_cast<const BookmarkNode*>([representedObject pointerValue]);
322   DCHECK(newParent);
323   if (oldParent != newParent) {
324     int index = newParent->child_count();
325     model_->Move(node_, newParent, index);
326     content::RecordAction(UserMetricsAction("BookmarkBubble_ChangeParent"));
327   }
330 // Fill in all information related to the folder pop up button.
331 - (void)fillInFolderList {
332   [nameTextField_ setStringValue:base::SysUTF16ToNSString(node_->GetTitle())];
333   DCHECK([folderPopUpButton_ numberOfItems] == 0);
334   [self addFolderNodes:model_->root_node()
335          toPopUpButton:folderPopUpButton_
336            indentation:0];
337   NSMenu* menu = [folderPopUpButton_ menu];
338   [menu addItem:[NSMenuItem separatorItem]];
339   NSString* title = [[self class] chooseAnotherFolderString];
340   NSMenuItem *item = [menu addItemWithTitle:title
341                                      action:NULL
342                               keyEquivalent:@""];
343   ChooseAnotherFolder* obj = [[self class] chooseAnotherFolderObject];
344   [item setRepresentedObject:obj];
345   // Finally, select the current parent.
346   NSValue* parentValue = [NSValue valueWithPointer:node_->parent()];
347   NSInteger idx = [menu indexOfItemWithRepresentedObject:parentValue];
348   [folderPopUpButton_ selectItemAtIndex:idx];
351 @end  // BookmarkBubbleController
354 @implementation BookmarkBubbleController (ExposedForUnitTesting)
356 - (NSView*)syncPromoPlaceholder {
357   return syncPromoPlaceholder_;
360 + (NSString*)chooseAnotherFolderString {
361   return l10n_util::GetNSStringWithFixup(
362       IDS_BOOKMARK_BUBBLE_CHOOSER_ANOTHER_FOLDER);
365 // For the given folder node, walk the tree and add folder names to
366 // the given pop up button.
367 - (void)addFolderNodes:(const BookmarkNode*)parent
368          toPopUpButton:(NSPopUpButton*)button
369            indentation:(int)indentation {
370   if (!model_->is_root_node(parent)) {
371     NSString* title = base::SysUTF16ToNSString(parent->GetTitle());
372     NSMenu* menu = [button menu];
373     NSMenuItem* item = [menu addItemWithTitle:title
374                                        action:NULL
375                                 keyEquivalent:@""];
376     [item setRepresentedObject:[NSValue valueWithPointer:parent]];
377     [item setIndentationLevel:indentation];
378     ++indentation;
379   }
380   for (int i = 0; i < parent->child_count(); i++) {
381     const BookmarkNode* child = parent->GetChild(i);
382     if (child->is_folder() && child->IsVisible() &&
383         client_->CanBeEditedByUser(child)) {
384       [self addFolderNodes:child
385              toPopUpButton:button
386                indentation:indentation];
387     }
388   }
391 - (void)setTitle:(NSString*)title parentFolder:(const BookmarkNode*)parent {
392   [nameTextField_ setStringValue:title];
393   [self setParentFolderSelection:parent];
396 // Pick a specific parent node in the selection by finding the right
397 // pop up button index.
398 - (void)setParentFolderSelection:(const BookmarkNode*)parent {
399   // Expectation: There is a parent mapping for all items in the
400   // folderPopUpButton except the last one ("Choose another folder...").
401   NSMenu* menu = [folderPopUpButton_ menu];
402   NSValue* parentValue = [NSValue valueWithPointer:parent];
403   NSInteger idx = [menu indexOfItemWithRepresentedObject:parentValue];
404   DCHECK(idx != -1);
405   [folderPopUpButton_ selectItemAtIndex:idx];
408 - (NSPopUpButton*)folderPopUpButton {
409   return folderPopUpButton_;
412 @end  // implementation BookmarkBubbleController(ExposedForUnitTesting)