437594448ca8e9192f344688ddbf385fbeb51f63
[platform/framework/web/crosswalk.git] / src / chrome / browser / ui / cocoa / bookmarks / bookmark_bubble_controller.mm
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.
4
5 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_bubble_controller.h"
6
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 "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 "grit/generated_resources.h"
25 #include "ui/base/l10n/l10n_util_mac.h"
26
27 using base::UserMetricsAction;
28
29 // An object to represent the ChooseAnotherFolder item in the pop up.
30 @interface ChooseAnotherFolder : NSObject
31 @end
32
33 @implementation ChooseAnotherFolder
34 @end
35
36 @interface BookmarkBubbleController (PrivateAPI)
37 - (void)updateBookmarkNode;
38 - (void)fillInFolderList;
39 @end
40
41 @implementation BookmarkBubbleController
42
43 @synthesize node = node_;
44
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;
49   if (!object) {
50     object = [[ChooseAnotherFolder alloc] init];
51   }
52   return object;
53 }
54
55 - (id)initWithParentWindow:(NSWindow*)parentWindow
56                     client:(ChromeBookmarkClient*)client
57                      model:(BookmarkModel*)model
58                       node:(const BookmarkNode*)node
59          alreadyBookmarked:(BOOL)alreadyBookmarked {
60   DCHECK(client);
61   DCHECK(node);
62   if ((self = [super initWithWindowNibPath:@"BookmarkBubble"
63                               parentWindow:parentWindow
64                                 anchoredAt:NSZeroPoint])) {
65     client_ = client;
66     model_ = model;
67     node_ = node;
68     alreadyBookmarked_ = alreadyBookmarked;
69   }
70   return self;
71 }
72
73 - (void)awakeFromNib {
74   [super awakeFromNib];
75
76   [[nameTextField_ cell] setUsesSingleLineMode:YES];
77
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]];
83
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;
89
90     [syncPromoPlaceholder_ setFrame:syncPromoPlaceholderFrame];
91     [[syncPromoController_ view] setFrame:syncPromoPlaceholderFrame];
92
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];
100   }
101 }
102
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  {
107   while (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,
119                              nil];
120       [[NSNotificationCenter defaultCenter]
121         postNotificationName:bookmark_button::kPulseBookmarkButtonNotification
122                       object:self
123                     userInfo:dict];
124       return;
125     }
126     node = node->parent();
127   }
128 }
129
130 - (void)stopPulsingBookmarkButton {
131   if (!pulsingBookmarkNode_)
132     return;
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,
142                          nil];
143   [[NSNotificationCenter defaultCenter]
144         postNotificationName:bookmark_button::kPulseBookmarkButtonNotification
145                       object:self
146                     userInfo:dict];
147 }
148
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 {
155   [self close];
156 }
157
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];
163 }
164
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];
173
174   InfoBubbleView* bubble = self.bubble;
175   [bubble setArrowLocation:info_bubble::kTopRight];
176
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];
187
188   // Default is IDS_BOOKMARK_BUBBLE_PAGE_BOOKMARK; "Bookmark".
189   // If adding for the 1st time the string becomes "Bookmark Added!"
190   if (!alreadyBookmarked_) {
191     NSString* title =
192         l10n_util::GetNSString(IDS_BOOKMARK_BUBBLE_PAGE_BOOKMARKED);
193     [bigTitle_ setStringValue:title];
194   }
195
196   [self fillInFolderList];
197
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
205           // is likely stale.
206           if (nodeWasDeleted)
207             pulsingBookmarkNode_ = NULL;
208           [self dismissWithoutEditing:nil];
209       }));
210   bookmarkObserver_->StartObservingNode(node_);
211
212   // Pulse something interesting on the bookmark bar.
213   [self startPulsingBookmarkButton:node_];
214
215   [parentWindow addChildWindow:window ordered:NSWindowAbove];
216   [window makeKeyAndOrderFront:self];
217   [self registerKeyStateEventTap];
218 }
219
220 - (void)close {
221   [[BrowserWindowController browserWindowControllerForWindow:self.parentWindow]
222       releaseBarVisibilityForOwner:self withAnimation:YES delay:NO];
223
224   [super close];
225 }
226
227 // Shows the bookmark editor sheet for more advanced editing.
228 - (void)showEditor {
229   [self ok:self];
230   // Send the action up through the responder chain.
231   [NSApp sendAction:@selector(editBookmarkNode:) to:nil from:self];
232 }
233
234 - (IBAction)edit:(id)sender {
235   content::RecordAction(UserMetricsAction("BookmarkBubble_Edit"));
236   [self showEditor];
237 }
238
239 - (IBAction)ok:(id)sender {
240   [self stopPulsingBookmarkButton];  // before parent changes
241   [self updateBookmarkNode];
242   [self close];
243 }
244
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];
252   } else {
253     [self ok:sender];
254   }
255 }
256
257 - (IBAction)remove:(id)sender {
258   [self stopPulsingBookmarkButton];
259   bookmarks::RemoveAllBookmarks(model_, node_->url());
260   content::RecordAction(UserMetricsAction("BookmarkBubble_Unstar"));
261   node_ = NULL;  // no longer valid
262   [self ok:sender];
263 }
264
265 // The controller is  the target of the pop up button box action so it can
266 // handle when "choose another folder" was picked.
267 - (IBAction)folderChanged:(id)sender {
268   DCHECK([sender isEqual:folderPopUpButton_]);
269   // It is possible that due to model change our parent window has been closed
270   // but the popup is still showing and able to notify the controller of a
271   // folder change.  We ignore the sender in this case.
272   if (!self.parentWindow)
273     return;
274   NSMenuItem* selected = [folderPopUpButton_ selectedItem];
275   ChooseAnotherFolder* chooseItem = [[self class] chooseAnotherFolderObject];
276   if ([[selected representedObject] isEqual:chooseItem]) {
277     content::RecordAction(
278         UserMetricsAction("BookmarkBubble_EditFromCombobox"));
279     [self showEditor];
280   }
281 }
282
283 // The controller is the delegate of the window so it receives did resign key
284 // notifications. When key is resigned mirror Windows behavior and close the
285 // window.
286 - (void)windowDidResignKey:(NSNotification*)notification {
287   NSWindow* window = [self window];
288   DCHECK_EQ([notification object], window);
289   if ([window isVisible]) {
290     // If the window isn't visible, it is already closed, and this notification
291     // has been sent as part of the closing operation, so no need to close.
292     [self ok:self];
293   }
294 }
295
296 // Look at the dialog; if the user has changed anything, update the
297 // bookmark node to reflect this.
298 - (void)updateBookmarkNode {
299   if (!node_) return;
300
301   // First the title...
302   NSString* oldTitle = base::SysUTF16ToNSString(node_->GetTitle());
303   NSString* newTitle = [nameTextField_ stringValue];
304   if (![oldTitle isEqual:newTitle]) {
305     model_->SetTitle(node_, base::SysNSStringToUTF16(newTitle));
306     content::RecordAction(
307         UserMetricsAction("BookmarkBubble_ChangeTitleInBubble"));
308   }
309   // Then the parent folder.
310   const BookmarkNode* oldParent = node_->parent();
311   NSMenuItem* selectedItem = [folderPopUpButton_ selectedItem];
312   id representedObject = [selectedItem representedObject];
313   if ([representedObject isEqual:[[self class] chooseAnotherFolderObject]]) {
314     // "Choose another folder..."
315     return;
316   }
317   const BookmarkNode* newParent =
318       static_cast<const BookmarkNode*>([representedObject pointerValue]);
319   DCHECK(newParent);
320   if (oldParent != newParent) {
321     int index = newParent->child_count();
322     model_->Move(node_, newParent, index);
323     content::RecordAction(UserMetricsAction("BookmarkBubble_ChangeParent"));
324   }
325 }
326
327 // Fill in all information related to the folder pop up button.
328 - (void)fillInFolderList {
329   [nameTextField_ setStringValue:base::SysUTF16ToNSString(node_->GetTitle())];
330   DCHECK([folderPopUpButton_ numberOfItems] == 0);
331   [self addFolderNodes:model_->root_node()
332          toPopUpButton:folderPopUpButton_
333            indentation:0];
334   NSMenu* menu = [folderPopUpButton_ menu];
335   [menu addItem:[NSMenuItem separatorItem]];
336   NSString* title = [[self class] chooseAnotherFolderString];
337   NSMenuItem *item = [menu addItemWithTitle:title
338                                      action:NULL
339                               keyEquivalent:@""];
340   ChooseAnotherFolder* obj = [[self class] chooseAnotherFolderObject];
341   [item setRepresentedObject:obj];
342   // Finally, select the current parent.
343   NSValue* parentValue = [NSValue valueWithPointer:node_->parent()];
344   NSInteger idx = [menu indexOfItemWithRepresentedObject:parentValue];
345   [folderPopUpButton_ selectItemAtIndex:idx];
346 }
347
348 @end  // BookmarkBubbleController
349
350
351 @implementation BookmarkBubbleController (ExposedForUnitTesting)
352
353 - (NSView*)syncPromoPlaceholder {
354   return syncPromoPlaceholder_;
355 }
356
357 + (NSString*)chooseAnotherFolderString {
358   return l10n_util::GetNSStringWithFixup(
359       IDS_BOOKMARK_BUBBLE_CHOOSER_ANOTHER_FOLDER);
360 }
361
362 // For the given folder node, walk the tree and add folder names to
363 // the given pop up button.
364 - (void)addFolderNodes:(const BookmarkNode*)parent
365          toPopUpButton:(NSPopUpButton*)button
366            indentation:(int)indentation {
367   if (!model_->is_root_node(parent)) {
368     NSString* title = base::SysUTF16ToNSString(parent->GetTitle());
369     NSMenu* menu = [button menu];
370     NSMenuItem* item = [menu addItemWithTitle:title
371                                        action:NULL
372                                 keyEquivalent:@""];
373     [item setRepresentedObject:[NSValue valueWithPointer:parent]];
374     [item setIndentationLevel:indentation];
375     ++indentation;
376   }
377   for (int i = 0; i < parent->child_count(); i++) {
378     const BookmarkNode* child = parent->GetChild(i);
379     if (child->is_folder() && child->IsVisible() &&
380         client_->CanBeEditedByUser(child)) {
381       [self addFolderNodes:child
382              toPopUpButton:button
383                indentation:indentation];
384     }
385   }
386 }
387
388 - (void)setTitle:(NSString*)title parentFolder:(const BookmarkNode*)parent {
389   [nameTextField_ setStringValue:title];
390   [self setParentFolderSelection:parent];
391 }
392
393 // Pick a specific parent node in the selection by finding the right
394 // pop up button index.
395 - (void)setParentFolderSelection:(const BookmarkNode*)parent {
396   // Expectation: There is a parent mapping for all items in the
397   // folderPopUpButton except the last one ("Choose another folder...").
398   NSMenu* menu = [folderPopUpButton_ menu];
399   NSValue* parentValue = [NSValue valueWithPointer:parent];
400   NSInteger idx = [menu indexOfItemWithRepresentedObject:parentValue];
401   DCHECK(idx != -1);
402   [folderPopUpButton_ selectItemAtIndex:idx];
403 }
404
405 - (NSPopUpButton*)folderPopUpButton {
406   return folderPopUpButton_;
407 }
408
409 @end  // implementation BookmarkBubbleController(ExposedForUnitTesting)