• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (c) 2011 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/mac_util.h"
8#include "base/sys_string_conversions.h"
9#include "base/utf_string_conversions.h"  // TODO(viettrungluu): remove
10#include "chrome/browser/bookmarks/bookmark_model.h"
11#include "chrome/browser/metrics/user_metrics.h"
12#import "chrome/browser/ui/cocoa/bookmarks/bookmark_button.h"
13#import "chrome/browser/ui/cocoa/browser_window_controller.h"
14#import "chrome/browser/ui/cocoa/info_bubble_view.h"
15#include "content/common/notification_observer.h"
16#include "content/common/notification_registrar.h"
17#include "content/common/notification_service.h"
18#include "grit/generated_resources.h"
19#include "ui/base/l10n/l10n_util_mac.h"
20
21
22// Simple class to watch for tab creation/destruction and close the bubble.
23// Bridge between Chrome-style notifications and ObjC-style notifications.
24class BookmarkBubbleNotificationBridge : public NotificationObserver {
25 public:
26  BookmarkBubbleNotificationBridge(BookmarkBubbleController* controller,
27                                   SEL selector);
28  virtual ~BookmarkBubbleNotificationBridge() {}
29  void Observe(NotificationType type,
30               const NotificationSource& source,
31               const NotificationDetails& details);
32 private:
33  NotificationRegistrar registrar_;
34  BookmarkBubbleController* controller_;  // weak; owns us.
35  SEL selector_;   // SEL sent to controller_ on notification.
36};
37
38BookmarkBubbleNotificationBridge::BookmarkBubbleNotificationBridge(
39  BookmarkBubbleController* controller, SEL selector)
40    : controller_(controller), selector_(selector) {
41  // registrar_ will automatically RemoveAll() when destroyed so we
42  // don't need to do so explicitly.
43  registrar_.Add(this, NotificationType::TAB_CONTENTS_CONNECTED,
44                 NotificationService::AllSources());
45  registrar_.Add(this, NotificationType::TAB_CLOSED,
46                 NotificationService::AllSources());
47}
48
49// At this time all notifications instigate the same behavior (go
50// away) so we don't bother checking which notification came in.
51void BookmarkBubbleNotificationBridge::Observe(
52  NotificationType type,
53  const NotificationSource& source,
54  const NotificationDetails& details) {
55  [controller_ performSelector:selector_ withObject:controller_];
56}
57
58
59// An object to represent the ChooseAnotherFolder item in the pop up.
60@interface ChooseAnotherFolder : NSObject
61@end
62
63@implementation ChooseAnotherFolder
64@end
65
66@interface BookmarkBubbleController (PrivateAPI)
67- (void)updateBookmarkNode;
68- (void)fillInFolderList;
69- (void)parentWindowWillClose:(NSNotification*)notification;
70@end
71
72@implementation BookmarkBubbleController
73
74@synthesize node = node_;
75
76+ (id)chooseAnotherFolderObject {
77  // Singleton object to act as a representedObject for the "choose another
78  // folder" item in the pop up.
79  static ChooseAnotherFolder* object = nil;
80  if (!object) {
81    object = [[ChooseAnotherFolder alloc] init];
82  }
83  return object;
84}
85
86- (id)initWithParentWindow:(NSWindow*)parentWindow
87                     model:(BookmarkModel*)model
88                      node:(const BookmarkNode*)node
89     alreadyBookmarked:(BOOL)alreadyBookmarked {
90  NSString* nibPath =
91      [base::mac::MainAppBundle() pathForResource:@"BookmarkBubble"
92                                          ofType:@"nib"];
93  if ((self = [super initWithWindowNibPath:nibPath owner:self])) {
94    parentWindow_ = parentWindow;
95    model_ = model;
96    node_ = node;
97    alreadyBookmarked_ = alreadyBookmarked;
98
99    // Watch to see if the parent window closes, and if so, close this one.
100    NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
101    [center addObserver:self
102               selector:@selector(parentWindowWillClose:)
103                   name:NSWindowWillCloseNotification
104                 object:parentWindow_];
105  }
106  return self;
107}
108
109- (void)dealloc {
110  [[NSNotificationCenter defaultCenter] removeObserver:self];
111  [super dealloc];
112}
113
114// If this is a new bookmark somewhere visible (e.g. on the bookmark
115// bar), pulse it.  Else, call ourself recursively with our parent
116// until we find something visible to pulse.
117- (void)startPulsingBookmarkButton:(const BookmarkNode*)node  {
118  while (node) {
119    if ((node->parent() == model_->GetBookmarkBarNode()) ||
120        (node == model_->other_node())) {
121      pulsingBookmarkNode_ = node;
122      NSValue *value = [NSValue valueWithPointer:node];
123      NSDictionary *dict = [NSDictionary
124                             dictionaryWithObjectsAndKeys:value,
125                             bookmark_button::kBookmarkKey,
126                             [NSNumber numberWithBool:YES],
127                             bookmark_button::kBookmarkPulseFlagKey,
128                             nil];
129      [[NSNotificationCenter defaultCenter]
130        postNotificationName:bookmark_button::kPulseBookmarkButtonNotification
131                      object:self
132                    userInfo:dict];
133      return;
134    }
135    node = node->parent();
136  }
137}
138
139- (void)stopPulsingBookmarkButton {
140  if (!pulsingBookmarkNode_)
141    return;
142  NSValue *value = [NSValue valueWithPointer:pulsingBookmarkNode_];
143  pulsingBookmarkNode_ = NULL;
144  NSDictionary *dict = [NSDictionary
145                         dictionaryWithObjectsAndKeys:value,
146                         bookmark_button::kBookmarkKey,
147                         [NSNumber numberWithBool:NO],
148                         bookmark_button::kBookmarkPulseFlagKey,
149                         nil];
150  [[NSNotificationCenter defaultCenter]
151        postNotificationName:bookmark_button::kPulseBookmarkButtonNotification
152                      object:self
153                    userInfo:dict];
154}
155
156// Close the bookmark bubble without changing anything.  Unlike a
157// typical dialog's OK/Cancel, where Cancel is "do nothing", all
158// buttons on the bubble have the capacity to change the bookmark
159// model.  This is an IBOutlet-looking entry point to remove the
160// dialog without touching the model.
161- (void)dismissWithoutEditing:(id)sender {
162  [self close];
163}
164
165- (void)parentWindowWillClose:(NSNotification*)notification {
166  [self close];
167}
168
169- (void)windowWillClose:(NSNotification*)notification {
170  // We caught a close so we don't need to watch for the parent closing.
171  [[NSNotificationCenter defaultCenter] removeObserver:self];
172  bookmark_observer_.reset(NULL);
173  chrome_observer_.reset(NULL);
174  [self stopPulsingBookmarkButton];
175  [self autorelease];
176}
177
178// We want this to be a child of a browser window.  addChildWindow:
179// (called from this function) will bring the window on-screen;
180// unfortunately, [NSWindowController showWindow:] will also bring it
181// on-screen (but will cause unexpected changes to the window's
182// position).  We cannot have an addChildWindow: and a subsequent
183// showWindow:. Thus, we have our own version.
184- (void)showWindow:(id)sender {
185  BrowserWindowController* bwc =
186      [BrowserWindowController browserWindowControllerForWindow:parentWindow_];
187  [bwc lockBarVisibilityForOwner:self withAnimation:NO delay:NO];
188  NSWindow* window = [self window];  // completes nib load
189  [bubble_ setArrowLocation:info_bubble::kTopRight];
190  // Insure decent positioning even in the absence of a browser controller,
191  // which will occur for some unit tests.
192  NSPoint arrowtip = bwc ? [bwc bookmarkBubblePoint] :
193      NSMakePoint([window frame].size.width, [window frame].size.height);
194  NSPoint origin = [parentWindow_ convertBaseToScreen:arrowtip];
195  NSPoint bubbleArrowtip = [bubble_ arrowTip];
196  bubbleArrowtip = [bubble_ convertPoint:bubbleArrowtip toView:nil];
197  origin.y -= bubbleArrowtip.y;
198  origin.x -= bubbleArrowtip.x;
199  [window setFrameOrigin:origin];
200  [parentWindow_ addChildWindow:window ordered:NSWindowAbove];
201  // Default is IDS_BOOMARK_BUBBLE_PAGE_BOOKMARK; "Bookmark".
202  // If adding for the 1st time the string becomes "Bookmark Added!"
203  if (!alreadyBookmarked_) {
204    NSString* title =
205        l10n_util::GetNSString(IDS_BOOMARK_BUBBLE_PAGE_BOOKMARKED);
206    [bigTitle_ setStringValue:title];
207  }
208
209  [self fillInFolderList];
210
211  // Ping me when things change out from under us.  Unlike a normal
212  // dialog, the bookmark bubble's cancel: means "don't add this as a
213  // bookmark", not "cancel editing".  We must take extra care to not
214  // touch the bookmark in this selector.
215  bookmark_observer_.reset(new BookmarkModelObserverForCocoa(
216                               node_, model_,
217                               self,
218                               @selector(dismissWithoutEditing:)));
219  chrome_observer_.reset(new BookmarkBubbleNotificationBridge(
220                             self, @selector(dismissWithoutEditing:)));
221
222  // Pulse something interesting on the bookmark bar.
223  [self startPulsingBookmarkButton:node_];
224
225  [window makeKeyAndOrderFront:self];
226}
227
228- (void)close {
229  [[BrowserWindowController browserWindowControllerForWindow:parentWindow_]
230      releaseBarVisibilityForOwner:self withAnimation:YES delay:NO];
231  [parentWindow_ removeChildWindow:[self window]];
232
233  // If you quit while the bubble is open, sometimes we get a
234  // DidResignKey before we get our parent's WindowWillClose and
235  // sometimes not.  We protect against a multiple close (or reference
236  // to parentWindow_ at a bad time) by clearing it out once we're
237  // done, and by removing ourself from future notifications.
238  [[NSNotificationCenter defaultCenter]
239    removeObserver:self
240              name:NSWindowWillCloseNotification
241            object:parentWindow_];
242  parentWindow_ = nil;
243
244  [super close];
245}
246
247// Shows the bookmark editor sheet for more advanced editing.
248- (void)showEditor {
249  [self ok:self];
250  // Send the action up through the responder chain.
251  [NSApp sendAction:@selector(editBookmarkNode:) to:nil from:self];
252}
253
254- (IBAction)edit:(id)sender {
255  UserMetrics::RecordAction(UserMetricsAction("BookmarkBubble_Edit"),
256                            model_->profile());
257  [self showEditor];
258}
259
260- (IBAction)ok:(id)sender {
261  [self stopPulsingBookmarkButton];  // before parent changes
262  [self updateBookmarkNode];
263  [self close];
264}
265
266// By implementing this, ESC causes the window to go away. If clicking the
267// star was what prompted this bubble to appear (i.e., not already bookmarked),
268// remove the bookmark.
269- (IBAction)cancel:(id)sender {
270  if (!alreadyBookmarked_) {
271    // |-remove:| calls |-close| so don't do it.
272    [self remove:sender];
273  } else {
274    [self ok:sender];
275  }
276}
277
278- (IBAction)remove:(id)sender {
279  [self stopPulsingBookmarkButton];
280  // TODO(viettrungluu): get rid of conversion and utf_string_conversions.h.
281  model_->SetURLStarred(node_->GetURL(), node_->GetTitle(), false);
282  UserMetrics::RecordAction(UserMetricsAction("BookmarkBubble_Unstar"),
283                            model_->profile());
284  node_ = NULL;  // no longer valid
285  [self ok:sender];
286}
287
288// The controller is  the target of the pop up button box action so it can
289// handle when "choose another folder" was picked.
290- (IBAction)folderChanged:(id)sender {
291  DCHECK([sender isEqual:folderPopUpButton_]);
292  // It is possible that due to model change our parent window has been closed
293  // but the popup is still showing and able to notify the controller of a
294  // folder change.  We ignore the sender in this case.
295  if (!parentWindow_)
296    return;
297  NSMenuItem* selected = [folderPopUpButton_ selectedItem];
298  ChooseAnotherFolder* chooseItem = [[self class] chooseAnotherFolderObject];
299  if ([[selected representedObject] isEqual:chooseItem]) {
300    UserMetrics::RecordAction(
301        UserMetricsAction("BookmarkBubble_EditFromCombobox"),
302        model_->profile());
303    [self showEditor];
304  }
305}
306
307// The controller is the delegate of the window so it receives did resign key
308// notifications. When key is resigned mirror Windows behavior and close the
309// window.
310- (void)windowDidResignKey:(NSNotification*)notification {
311  NSWindow* window = [self window];
312  DCHECK_EQ([notification object], window);
313  if ([window isVisible]) {
314    // If the window isn't visible, it is already closed, and this notification
315    // has been sent as part of the closing operation, so no need to close.
316    [self ok:self];
317  }
318}
319
320// Look at the dialog; if the user has changed anything, update the
321// bookmark node to reflect this.
322- (void)updateBookmarkNode {
323  if (!node_) return;
324
325  // First the title...
326  NSString* oldTitle = base::SysUTF16ToNSString(node_->GetTitle());
327  NSString* newTitle = [nameTextField_ stringValue];
328  if (![oldTitle isEqual:newTitle]) {
329    model_->SetTitle(node_, base::SysNSStringToUTF16(newTitle));
330    UserMetrics::RecordAction(
331        UserMetricsAction("BookmarkBubble_ChangeTitleInBubble"),
332        model_->profile());
333  }
334  // Then the parent folder.
335  const BookmarkNode* oldParent = node_->parent();
336  NSMenuItem* selectedItem = [folderPopUpButton_ selectedItem];
337  id representedObject = [selectedItem representedObject];
338  if ([representedObject isEqual:[[self class] chooseAnotherFolderObject]]) {
339    // "Choose another folder..."
340    return;
341  }
342  const BookmarkNode* newParent =
343      static_cast<const BookmarkNode*>([representedObject pointerValue]);
344  DCHECK(newParent);
345  if (oldParent != newParent) {
346    int index = newParent->child_count();
347    model_->Move(node_, newParent, index);
348    UserMetrics::RecordAction(UserMetricsAction("BookmarkBubble_ChangeParent"),
349                              model_->profile());
350  }
351}
352
353// Fill in all information related to the folder pop up button.
354- (void)fillInFolderList {
355  [nameTextField_ setStringValue:base::SysUTF16ToNSString(node_->GetTitle())];
356  DCHECK([folderPopUpButton_ numberOfItems] == 0);
357  [self addFolderNodes:model_->root_node()
358         toPopUpButton:folderPopUpButton_
359           indentation:0];
360  NSMenu* menu = [folderPopUpButton_ menu];
361  NSString* title = [[self class] chooseAnotherFolderString];
362  NSMenuItem *item = [menu addItemWithTitle:title
363                                     action:NULL
364                              keyEquivalent:@""];
365  ChooseAnotherFolder* obj = [[self class] chooseAnotherFolderObject];
366  [item setRepresentedObject:obj];
367  // Finally, select the current parent.
368  NSValue* parentValue = [NSValue valueWithPointer:node_->parent()];
369  NSInteger idx = [menu indexOfItemWithRepresentedObject:parentValue];
370  [folderPopUpButton_ selectItemAtIndex:idx];
371}
372
373@end  // BookmarkBubbleController
374
375
376@implementation BookmarkBubbleController(ExposedForUnitTesting)
377
378+ (NSString*)chooseAnotherFolderString {
379  return l10n_util::GetNSStringWithFixup(
380      IDS_BOOMARK_BUBBLE_CHOOSER_ANOTHER_FOLDER);
381}
382
383// For the given folder node, walk the tree and add folder names to
384// the given pop up button.
385- (void)addFolderNodes:(const BookmarkNode*)parent
386         toPopUpButton:(NSPopUpButton*)button
387           indentation:(int)indentation {
388  if (!model_->is_root(parent))  {
389    NSString* title = base::SysUTF16ToNSString(parent->GetTitle());
390    NSMenu* menu = [button menu];
391    NSMenuItem* item = [menu addItemWithTitle:title
392                                       action:NULL
393                                keyEquivalent:@""];
394    [item setRepresentedObject:[NSValue valueWithPointer:parent]];
395    [item setIndentationLevel:indentation];
396    ++indentation;
397  }
398  for (int i = 0; i < parent->child_count(); i++) {
399    const BookmarkNode* child = parent->GetChild(i);
400    if (child->is_folder())
401      [self addFolderNodes:child
402             toPopUpButton:button
403               indentation:indentation];
404  }
405}
406
407- (void)setTitle:(NSString*)title parentFolder:(const BookmarkNode*)parent {
408  [nameTextField_ setStringValue:title];
409  [self setParentFolderSelection:parent];
410}
411
412// Pick a specific parent node in the selection by finding the right
413// pop up button index.
414- (void)setParentFolderSelection:(const BookmarkNode*)parent {
415  // Expectation: There is a parent mapping for all items in the
416  // folderPopUpButton except the last one ("Choose another folder...").
417  NSMenu* menu = [folderPopUpButton_ menu];
418  NSValue* parentValue = [NSValue valueWithPointer:parent];
419  NSInteger idx = [menu indexOfItemWithRepresentedObject:parentValue];
420  DCHECK(idx != -1);
421  [folderPopUpButton_ selectItemAtIndex:idx];
422}
423
424- (NSPopUpButton*)folderPopUpButton {
425  return folderPopUpButton_;
426}
427
428@end  // implementation BookmarkBubbleController(ExposedForUnitTesting)
429