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