Home | History | Annotate | Download | only in find_bar
      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 <Cocoa/Cocoa.h>
      6 
      7 #include "base/mac/mac_util.h"
      8 #include "base/sys_string_conversions.h"
      9 #include "chrome/browser/ui/cocoa/browser_window_controller.h"
     10 #import "chrome/browser/ui/cocoa/find_bar/find_bar_cocoa_controller.h"
     11 #import "chrome/browser/ui/cocoa/find_bar/find_bar_bridge.h"
     12 #import "chrome/browser/ui/cocoa/find_bar/find_bar_text_field.h"
     13 #import "chrome/browser/ui/cocoa/find_bar/find_bar_text_field_cell.h"
     14 #import "chrome/browser/ui/cocoa/find_pasteboard.h"
     15 #import "chrome/browser/ui/cocoa/focus_tracker.h"
     16 #import "chrome/browser/ui/cocoa/nsview_additions.h"
     17 #import "chrome/browser/ui/cocoa/tabs/tab_strip_controller.h"
     18 #include "chrome/browser/ui/find_bar/find_bar_controller.h"
     19 #include "chrome/browser/ui/find_bar/find_tab_helper.h"
     20 #include "chrome/browser/ui/tab_contents/tab_contents_wrapper.h"
     21 #include "content/browser/renderer_host/render_view_host.h"
     22 #include "content/browser/tab_contents/tab_contents.h"
     23 #include "content/browser/tab_contents/tab_contents_view.h"
     24 #import "third_party/GTM/AppKit/GTMNSAnimation+Duration.h"
     25 
     26 const float kFindBarOpenDuration = 0.2;
     27 const float kFindBarCloseDuration = 0.15;
     28 const float kFindBarMoveDuration = 0.15;
     29 const float kRightEdgeOffset = 25;
     30 
     31 @interface FindBarCocoaController (PrivateMethods) <NSAnimationDelegate>
     32 // Returns the appropriate frame for a hidden find bar.
     33 - (NSRect)hiddenFindBarFrame;
     34 
     35 // Animates the given |view| to the given |endFrame| within |duration| seconds.
     36 // Returns a new NSViewAnimation.
     37 - (NSViewAnimation*)createAnimationForView:(NSView*)view
     38                                    toFrame:(NSRect)endFrame
     39                                   duration:(float)duration;
     40 
     41 // Sets the frame of |findBarView_|.  |duration| is ignored if |animate| is NO.
     42 - (void)setFindBarFrame:(NSRect)endFrame
     43                 animate:(BOOL)animate
     44                duration:(float)duration;
     45 
     46 // Returns the horizontal position the FindBar should use in order to avoid
     47 // overlapping with the current find result, if there's one.
     48 - (float)findBarHorizontalPosition;
     49 
     50 // Adjusts the horizontal position if necessary to avoid overlapping with the
     51 // current find result.
     52 - (void)moveFindBarIfNecessary:(BOOL)animate;
     53 
     54 // Optionally stops the current search, puts |text| into the find bar, and
     55 // enables the buttons, but doesn't start a new search for |text|.
     56 - (void)prepopulateText:(NSString*)text stopSearch:(BOOL)stopSearch;
     57 @end
     58 
     59 @implementation FindBarCocoaController
     60 
     61 - (id)init {
     62   if ((self = [super initWithNibName:@"FindBar"
     63                               bundle:base::mac::MainAppBundle()])) {
     64     [[NSNotificationCenter defaultCenter]
     65         addObserver:self
     66            selector:@selector(findPboardUpdated:)
     67                name:kFindPasteboardChangedNotification
     68              object:[FindPasteboard sharedInstance]];
     69   }
     70   return self;
     71 }
     72 
     73 - (void)dealloc {
     74   // All animations should be explicitly stopped by the TabContents before a tab
     75   // is closed.
     76   DCHECK(!showHideAnimation_.get());
     77   DCHECK(!moveAnimation_.get());
     78   [[NSNotificationCenter defaultCenter] removeObserver:self];
     79   [super dealloc];
     80 }
     81 
     82 - (void)setFindBarBridge:(FindBarBridge*)findBarBridge {
     83   DCHECK(!findBarBridge_);  // should only be called once.
     84   findBarBridge_ = findBarBridge;
     85 }
     86 
     87 - (void)setBrowserWindowController:(BrowserWindowController*)controller {
     88   DCHECK(!browserWindowController_); // should only be called once.
     89   browserWindowController_ = controller;
     90 }
     91 
     92 - (void)awakeFromNib {
     93   [findBarView_ setFrame:[self hiddenFindBarFrame]];
     94 
     95   // Stopping the search requires a findbar controller, which isn't valid yet
     96   // during setup. Furthermore, there is no active search yet anyway.
     97   [self prepopulateText:[[FindPasteboard sharedInstance] findText]
     98              stopSearch:NO];
     99 }
    100 
    101 - (IBAction)close:(id)sender {
    102   if (findBarBridge_)
    103     findBarBridge_->GetFindBarController()->EndFindSession(
    104         FindBarController::kKeepSelection);
    105 }
    106 
    107 - (IBAction)previousResult:(id)sender {
    108   if (findBarBridge_) {
    109     FindTabHelper* find_tab_helper = findBarBridge_->
    110         GetFindBarController()->tab_contents()->find_tab_helper();
    111     find_tab_helper->StartFinding(
    112         base::SysNSStringToUTF16([findText_ stringValue]),
    113         false, false);
    114   }
    115 }
    116 
    117 - (IBAction)nextResult:(id)sender {
    118   if (findBarBridge_) {
    119     FindTabHelper* find_tab_helper = findBarBridge_->
    120         GetFindBarController()->tab_contents()->find_tab_helper();
    121     find_tab_helper->StartFinding(
    122         base::SysNSStringToUTF16([findText_ stringValue]),
    123         true, false);
    124   }
    125 }
    126 
    127 - (void)findPboardUpdated:(NSNotification*)notification {
    128   if (suppressPboardUpdateActions_)
    129     return;
    130   [self prepopulateText:[[FindPasteboard sharedInstance] findText]
    131              stopSearch:YES];
    132 }
    133 
    134 - (void)positionFindBarViewAtMaxY:(CGFloat)maxY maxWidth:(CGFloat)maxWidth {
    135   NSView* containerView = [self view];
    136   CGFloat containerHeight = NSHeight([containerView frame]);
    137   CGFloat containerWidth = NSWidth([containerView frame]);
    138 
    139   // Adjust where we'll actually place the find bar.
    140   maxY += [containerView cr_lineWidth];
    141   maxY_ = maxY;
    142   CGFloat x = [self findBarHorizontalPosition];
    143   NSRect newFrame = NSMakeRect(x, maxY - containerHeight,
    144                                containerWidth, containerHeight);
    145 
    146   if (moveAnimation_.get() != nil) {
    147     NSRect frame = [containerView frame];
    148     [moveAnimation_ stopAnimation];
    149     // Restore to the X position before the animation was stopped. The Y
    150     // position is immediately adjusted.
    151     frame.origin.y = newFrame.origin.y;
    152     [containerView setFrame:frame];
    153     moveAnimation_.reset([self createAnimationForView:containerView
    154                                               toFrame:newFrame
    155                                              duration:kFindBarMoveDuration]);
    156   } else {
    157     [containerView setFrame:newFrame];
    158   }
    159 }
    160 
    161 // NSControl delegate method.
    162 - (void)controlTextDidChange:(NSNotification *)aNotification {
    163   if (!findBarBridge_)
    164     return;
    165 
    166   TabContentsWrapper* tab_contents =
    167       findBarBridge_->GetFindBarController()->tab_contents();
    168   if (!tab_contents)
    169     return;
    170   FindTabHelper* find_tab_helper = tab_contents->find_tab_helper();
    171 
    172   NSString* findText = [findText_ stringValue];
    173   suppressPboardUpdateActions_ = YES;
    174   [[FindPasteboard sharedInstance] setFindText:findText];
    175   suppressPboardUpdateActions_ = NO;
    176 
    177   if ([findText length] > 0) {
    178     find_tab_helper->
    179         StartFinding(base::SysNSStringToUTF16(findText), true, false);
    180   } else {
    181     // The textbox is empty so we reset.
    182     find_tab_helper->StopFinding(FindBarController::kClearSelection);
    183     [self updateUIForFindResult:find_tab_helper->find_result()
    184                        withText:string16()];
    185   }
    186 }
    187 
    188 // NSControl delegate method
    189 - (BOOL)control:(NSControl*)control
    190     textView:(NSTextView*)textView
    191     doCommandBySelector:(SEL)command {
    192   if (command == @selector(insertNewline:)) {
    193     // Pressing Return
    194     NSEvent* event = [NSApp currentEvent];
    195 
    196     if ([event modifierFlags] & NSShiftKeyMask)
    197       [previousButton_ performClick:nil];
    198     else
    199       [nextButton_ performClick:nil];
    200 
    201     return YES;
    202   } else if (command == @selector(insertLineBreak:)) {
    203     // Pressing Ctrl-Return
    204     if (findBarBridge_) {
    205       findBarBridge_->GetFindBarController()->EndFindSession(
    206           FindBarController::kActivateSelection);
    207     }
    208     return YES;
    209   } else if (command == @selector(pageUp:) ||
    210              command == @selector(pageUpAndModifySelection:) ||
    211              command == @selector(scrollPageUp:) ||
    212              command == @selector(pageDown:) ||
    213              command == @selector(pageDownAndModifySelection:) ||
    214              command == @selector(scrollPageDown:) ||
    215              command == @selector(scrollToBeginningOfDocument:) ||
    216              command == @selector(scrollToEndOfDocument:) ||
    217              command == @selector(moveUp:) ||
    218              command == @selector(moveDown:)) {
    219     TabContentsWrapper* contents =
    220         findBarBridge_->GetFindBarController()->tab_contents();
    221     if (!contents)
    222       return NO;
    223 
    224     // Sanity-check to make sure we got a keyboard event.
    225     NSEvent* event = [NSApp currentEvent];
    226     if ([event type] != NSKeyDown && [event type] != NSKeyUp)
    227       return NO;
    228 
    229     // Forward the event to the renderer.
    230     // TODO(rohitrao): Should this call -[BaseView keyEvent:]?  Is there code in
    231     // that function that we want to keep or avoid? Calling
    232     // |ForwardKeyboardEvent()| directly ignores edit commands, which breaks
    233     // cmd-up/down if we ever decide to include |moveToBeginningOfDocument:| in
    234     // the list above.
    235     RenderViewHost* render_view_host = contents->render_view_host();
    236     render_view_host->ForwardKeyboardEvent(NativeWebKeyboardEvent(event));
    237     return YES;
    238   }
    239 
    240   return NO;
    241 }
    242 
    243 // Methods from FindBar
    244 - (void)showFindBar:(BOOL)animate {
    245   // Save the currently-focused view.  |findBarView_| is in the view
    246   // hierarchy by now.  showFindBar can be called even when the
    247   // findbar is already open, so do not overwrite an already saved
    248   // view.
    249   if (!focusTracker_.get())
    250     focusTracker_.reset(
    251         [[FocusTracker alloc] initWithWindow:[findBarView_ window]]);
    252 
    253   // The browser window might have changed while the FindBar was hidden.
    254   // Update its position now.
    255   [browserWindowController_ layoutSubviews];
    256 
    257   // Move to the correct horizontal position first, to prevent the FindBar
    258   // from jumping around when switching tabs.
    259   // Prevent jumping while the FindBar is animating (hiding, then showing) too.
    260   if (![self isFindBarVisible])
    261     [self moveFindBarIfNecessary:NO];
    262 
    263   // Animate the view into place.
    264   NSRect frame = [findBarView_ frame];
    265   frame.origin = NSMakePoint(0, 0);
    266   [self setFindBarFrame:frame animate:animate duration:kFindBarOpenDuration];
    267 }
    268 
    269 - (void)hideFindBar:(BOOL)animate {
    270   NSRect frame = [self hiddenFindBarFrame];
    271   [self setFindBarFrame:frame animate:animate duration:kFindBarCloseDuration];
    272 }
    273 
    274 - (void)stopAnimation {
    275   if (showHideAnimation_.get()) {
    276     [showHideAnimation_ stopAnimation];
    277     showHideAnimation_.reset(nil);
    278   }
    279   if (moveAnimation_.get()) {
    280     [moveAnimation_ stopAnimation];
    281     moveAnimation_.reset(nil);
    282   }
    283 }
    284 
    285 - (void)setFocusAndSelection {
    286   [[findText_ window] makeFirstResponder:findText_];
    287 
    288   // Enable the buttons if the find text is non-empty.
    289   BOOL buttonsEnabled = ([[findText_ stringValue] length] > 0) ? YES : NO;
    290   [previousButton_ setEnabled:buttonsEnabled];
    291   [nextButton_ setEnabled:buttonsEnabled];
    292 }
    293 
    294 - (void)restoreSavedFocus {
    295   if (!(focusTracker_.get() &&
    296         [focusTracker_ restoreFocusInWindow:[findBarView_ window]])) {
    297     // Fall back to giving focus to the tab contents.
    298     findBarBridge_->
    299         GetFindBarController()->tab_contents()->tab_contents()->Focus();
    300   }
    301   focusTracker_.reset(nil);
    302 }
    303 
    304 - (void)setFindText:(NSString*)findText {
    305   [findText_ setStringValue:findText];
    306 
    307   // Make sure the text in the find bar always ends up in the find pasteboard
    308   // (and, via notifications, in the other find bars too).
    309   [[FindPasteboard sharedInstance] setFindText:findText];
    310 }
    311 
    312 - (void)clearResults:(const FindNotificationDetails&)results {
    313   // Just call updateUIForFindResult, which will take care of clearing
    314   // the search text and the results label.
    315   [self updateUIForFindResult:results withText:string16()];
    316 }
    317 
    318 - (void)updateUIForFindResult:(const FindNotificationDetails&)result
    319                      withText:(const string16&)findText {
    320   // If we don't have any results and something was passed in, then
    321   // that means someone pressed Cmd-G while the Find box was
    322   // closed. In that case we need to repopulate the Find box with what
    323   // was passed in.
    324   if ([[findText_ stringValue] length] == 0 && !findText.empty()) {
    325     [findText_ setStringValue:base::SysUTF16ToNSString(findText)];
    326     [findText_ selectText:self];
    327   }
    328 
    329   // Make sure Find Next and Find Previous are enabled if we found any matches.
    330   BOOL buttonsEnabled = result.number_of_matches() > 0 ? YES : NO;
    331   [previousButton_ setEnabled:buttonsEnabled];
    332   [nextButton_ setEnabled:buttonsEnabled];
    333 
    334   // Update the results label.
    335   BOOL validRange = result.active_match_ordinal() != -1 &&
    336                     result.number_of_matches() != -1;
    337   NSString* searchString = [findText_ stringValue];
    338   if ([searchString length] > 0 && validRange) {
    339     [[findText_ findBarTextFieldCell]
    340         setActiveMatch:result.active_match_ordinal()
    341                     of:result.number_of_matches()];
    342   } else {
    343     // If there was no text entered, we don't show anything in the results area.
    344     [[findText_ findBarTextFieldCell] clearResults];
    345   }
    346 
    347   [findText_ resetFieldEditorFrameIfNeeded];
    348 
    349   // If we found any results, reset the focus tracker, so we always
    350   // restore focus to the tab contents.
    351   if (result.number_of_matches() > 0)
    352     focusTracker_.reset(nil);
    353 
    354   // Adjust the FindBar position, even when there are no matches (so that it
    355   // goes back to the default position, if required).
    356   [self moveFindBarIfNecessary:[self isFindBarVisible]];
    357 }
    358 
    359 - (BOOL)isFindBarVisible {
    360   // Find bar is visible if any part of it is on the screen.
    361   return NSIntersectsRect([[self view] bounds], [findBarView_ frame]);
    362 }
    363 
    364 - (BOOL)isFindBarAnimating {
    365   return (showHideAnimation_.get() != nil) || (moveAnimation_.get() != nil);
    366 }
    367 
    368 // NSAnimation delegate methods.
    369 - (void)animationDidEnd:(NSAnimation*)animation {
    370   // Autorelease the animations (cannot use release because the animation object
    371   // is still on the stack.
    372   if (animation == showHideAnimation_.get()) {
    373     [showHideAnimation_.release() autorelease];
    374   } else if (animation == moveAnimation_.get()) {
    375     [moveAnimation_.release() autorelease];
    376   } else {
    377     NOTREACHED();
    378   }
    379 
    380   // If the find bar is not visible, make it actually hidden, so it'll no longer
    381   // respond to key events.
    382   [findBarView_ setHidden:![self isFindBarVisible]];
    383 }
    384 
    385 - (gfx::Point)findBarWindowPosition {
    386   gfx::Rect view_rect(NSRectToCGRect([[self view] frame]));
    387   // Convert Cocoa coordinates (Y growing up) to Y growing down.
    388   // Offset from |maxY_|, which represents the content view's top, instead
    389   // of from the superview, which represents the whole browser window.
    390   view_rect.set_y(maxY_ - view_rect.bottom());
    391   return view_rect.origin();
    392 }
    393 
    394 @end
    395 
    396 @implementation FindBarCocoaController (PrivateMethods)
    397 
    398 - (NSRect)hiddenFindBarFrame {
    399   NSRect frame = [findBarView_ frame];
    400   NSRect containerBounds = [[self view] bounds];
    401   frame.origin = NSMakePoint(NSMinX(containerBounds), NSMaxY(containerBounds));
    402   return frame;
    403 }
    404 
    405 - (NSViewAnimation*)createAnimationForView:(NSView*)view
    406                                    toFrame:(NSRect)endFrame
    407                                   duration:(float)duration {
    408   NSDictionary* dict = [NSDictionary dictionaryWithObjectsAndKeys:
    409       view, NSViewAnimationTargetKey,
    410       [NSValue valueWithRect:endFrame], NSViewAnimationEndFrameKey, nil];
    411 
    412   NSViewAnimation* animation =
    413       [[NSViewAnimation alloc]
    414         initWithViewAnimations:[NSArray arrayWithObjects:dict, nil]];
    415   [animation gtm_setDuration:duration
    416                            eventMask:NSLeftMouseUpMask];
    417   [animation setDelegate:self];
    418   [animation startAnimation];
    419   return animation;
    420 }
    421 
    422 - (void)setFindBarFrame:(NSRect)endFrame
    423                 animate:(BOOL)animate
    424                duration:(float)duration {
    425   // Save the current frame.
    426   NSRect startFrame = [findBarView_ frame];
    427 
    428   // Stop any existing animations.
    429   [showHideAnimation_ stopAnimation];
    430 
    431   if (!animate) {
    432     [findBarView_ setFrame:endFrame];
    433     [findBarView_ setHidden:![self isFindBarVisible]];
    434     showHideAnimation_.reset(nil);
    435     return;
    436   }
    437 
    438   // If animating, ensure that the find bar is not hidden. Hidden status will be
    439   // updated at the end of the animation.
    440   [findBarView_ setHidden:NO];
    441 
    442   // Reset the frame to what was saved above.
    443   [findBarView_ setFrame:startFrame];
    444 
    445   showHideAnimation_.reset([self createAnimationForView:findBarView_
    446                                                 toFrame:endFrame
    447                                                duration:duration]);
    448 }
    449 
    450 - (float)findBarHorizontalPosition {
    451   // Get the rect of the FindBar.
    452   NSView* view = [self view];
    453   NSRect frame = [view frame];
    454   gfx::Rect view_rect(NSRectToCGRect(frame));
    455 
    456   if (!findBarBridge_ || !findBarBridge_->GetFindBarController())
    457     return frame.origin.x;
    458   TabContentsWrapper* contents =
    459       findBarBridge_->GetFindBarController()->tab_contents();
    460   if (!contents)
    461     return frame.origin.x;
    462 
    463   // Get the size of the container.
    464   gfx::Rect container_rect(contents->view()->GetContainerSize());
    465 
    466   // Position the FindBar on the top right corner.
    467   view_rect.set_x(
    468       container_rect.width() - view_rect.width() - kRightEdgeOffset);
    469   // Convert from Cocoa coordinates (Y growing up) to Y growing down.
    470   // Notice that the view frame's Y offset is relative to the whole window,
    471   // while GetLocationForFindbarView() expects it relative to the
    472   // content's boundaries. |maxY_| has the correct placement in Cocoa coords,
    473   // so we just have to invert the Y coordinate.
    474   view_rect.set_y(maxY_ - view_rect.bottom());
    475 
    476   // Get the rect of the current find result, if there is one.
    477   const FindNotificationDetails& find_result =
    478       contents->find_tab_helper()->find_result();
    479   if (find_result.number_of_matches() == 0)
    480     return view_rect.x();
    481   gfx::Rect selection_rect(find_result.selection_rect());
    482 
    483   // Adjust |view_rect| to avoid the |selection_rect| within |container_rect|.
    484   gfx::Rect new_pos = FindBarController::GetLocationForFindbarView(
    485       view_rect, container_rect, selection_rect);
    486 
    487   return new_pos.x();
    488 }
    489 
    490 - (void)moveFindBarIfNecessary:(BOOL)animate {
    491   // Don't animate during tests.
    492   if (FindBarBridge::disable_animations_during_testing_)
    493     animate = NO;
    494 
    495   NSView* view = [self view];
    496   NSRect frame = [view frame];
    497   float x = [self findBarHorizontalPosition];
    498 
    499   if (animate) {
    500     [moveAnimation_ stopAnimation];
    501     // Restore to the position before the animation was stopped.
    502     [view setFrame:frame];
    503     frame.origin.x = x;
    504     moveAnimation_.reset([self createAnimationForView:view
    505                                               toFrame:frame
    506                                              duration:kFindBarMoveDuration]);
    507   } else {
    508     frame.origin.x = x;
    509     [view setFrame:frame];
    510   }
    511 }
    512 
    513 - (void)prepopulateText:(NSString*)text stopSearch:(BOOL)stopSearch{
    514   [self setFindText:text];
    515 
    516   // End the find session, hide the "x of y" text and disable the
    517   // buttons, but do not close the find bar or raise the window here.
    518   if (stopSearch && findBarBridge_) {
    519     TabContentsWrapper* contents =
    520         findBarBridge_->GetFindBarController()->tab_contents();
    521     if (contents) {
    522       FindTabHelper* find_tab_helper = contents->find_tab_helper();
    523       find_tab_helper->StopFinding(FindBarController::kClearSelection);
    524       findBarBridge_->ClearResults(find_tab_helper->find_result());
    525     }
    526   }
    527 
    528   // Has to happen after |ClearResults()| above.
    529   BOOL buttonsEnabled = [text length] > 0 ? YES : NO;
    530   [previousButton_ setEnabled:buttonsEnabled];
    531   [nextButton_ setEnabled:buttonsEnabled];
    532 }
    533 
    534 @end
    535