Home | History | Annotate | Download | only in bookmarks
      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.
     24 class 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 
     38 BookmarkBubbleNotificationBridge::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.
     51 void 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