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