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.
5 #import <Cocoa/Cocoa.h>
7 #include "base/auto_reset.h"
8 #include "base/mac/bundle_locations.h"
9 #include "base/strings/sys_string_conversions.h"
10 #include "chrome/browser/ui/browser_finder.h"
11 #include "chrome/browser/ui/browser_window.h"
12 #include "chrome/browser/ui/cocoa/browser_window_controller.h"
13 #import "chrome/browser/ui/cocoa/find_bar/find_bar_bridge.h"
14 #import "chrome/browser/ui/cocoa/find_bar/find_bar_cocoa_controller.h"
15 #import "chrome/browser/ui/cocoa/find_bar/find_bar_text_field.h"
16 #import "chrome/browser/ui/cocoa/find_bar/find_bar_text_field_cell.h"
17 #import "chrome/browser/ui/cocoa/image_button_cell.h"
18 #import "chrome/browser/ui/cocoa/tabs/tab_strip_controller.h"
19 #include "chrome/browser/ui/find_bar/find_bar_controller.h"
20 #include "chrome/browser/ui/find_bar/find_tab_helper.h"
21 #include "content/public/browser/render_view_host.h"
22 #include "content/public/browser/web_contents.h"
23 #include "grit/theme_resources.h"
24 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSAnimation+Duration.h"
25 #import "ui/base/cocoa/find_pasteboard.h"
26 #import "ui/base/cocoa/focus_tracker.h"
27 #import "ui/base/cocoa/nsview_additions.h"
28 #include "ui/resources/grit/ui_resources.h"
30 using content::NativeWebKeyboardEvent;
32 const float kFindBarOpenDuration = 0.2;
33 const float kFindBarCloseDuration = 0.15;
34 const float kFindBarMoveDuration = 0.15;
35 const float kRightEdgeOffset = 25;
38 @interface FindBarCocoaController (PrivateMethods) <NSAnimationDelegate>
39 // Returns the appropriate frame for a hidden find bar.
40 - (NSRect)hiddenFindBarFrame;
42 // Animates the given |view| to the given |endFrame| within |duration| seconds.
43 // Returns a new NSViewAnimation.
44 - (NSViewAnimation*)createAnimationForView:(NSView*)view
45 toFrame:(NSRect)endFrame
46 duration:(float)duration;
48 // Sets the frame of |findBarView_|. |duration| is ignored if |animate| is NO.
49 - (void)setFindBarFrame:(NSRect)endFrame
51 duration:(float)duration;
53 // Returns the horizontal position the FindBar should use in order to avoid
54 // overlapping with the current find result, if there's one.
55 - (float)findBarHorizontalPosition;
57 // Adjusts the horizontal position if necessary to avoid overlapping with the
58 // current find result.
59 - (void)moveFindBarIfNecessary:(BOOL)animate;
61 // Puts |text| into the find bar and enables the buttons, but doesn't start a
62 // new search for |text|.
63 - (void)prepopulateText:(NSString*)text;
65 // Clears the find results for all tabs in browser associated with this find
66 // bar. If |suppressPboardUpdateActions_| is true then the current tab is not
68 - (void)clearFindResultsForCurrentBrowser;
70 - (BrowserWindowController*)browserWindowController;
73 @implementation FindBarCocoaController
75 @synthesize findBarView = findBarView_;
77 - (id)initWithBrowser:(Browser*)browser {
78 if ((self = [super initWithNibName:@"FindBar"
79 bundle:base::mac::FrameworkBundle()])) {
80 [[NSNotificationCenter defaultCenter]
82 selector:@selector(findPboardUpdated:)
83 name:kFindPasteboardChangedNotification
84 object:[FindPasteboard sharedInstance]];
91 [self browserWillBeDestroyed];
95 - (void)browserWillBeDestroyed {
96 // All animations should have been explicitly stopped before a tab is closed.
97 DCHECK(!showHideAnimation_.get());
98 DCHECK(!moveAnimation_.get());
99 [[NSNotificationCenter defaultCenter] removeObserver:self];
103 - (void)setFindBarBridge:(FindBarBridge*)findBarBridge {
104 DCHECK(!findBarBridge_); // should only be called once.
105 findBarBridge_ = findBarBridge;
108 - (void)awakeFromNib {
109 [[closeButton_ cell] setImageID:IDR_CLOSE_1
110 forButtonState:image_button_cell::kDefaultState];
111 [[closeButton_ cell] setImageID:IDR_CLOSE_1_H
112 forButtonState:image_button_cell::kHoverState];
113 [[closeButton_ cell] setImageID:IDR_CLOSE_1_P
114 forButtonState:image_button_cell::kPressedState];
115 [[closeButton_ cell] setImageID:IDR_CLOSE_1
116 forButtonState:image_button_cell::kDisabledState];
118 [findBarView_ setFrame:[self hiddenFindBarFrame]];
119 defaultWidth_ = NSWidth([findBarView_ frame]);
120 [[self view] setHidden:YES];
122 [self prepopulateText:[[FindPasteboard sharedInstance] findText]];
125 - (IBAction)close:(id)sender {
127 findBarBridge_->GetFindBarController()->EndFindSession(
128 FindBarController::kKeepSelectionOnPage,
129 FindBarController::kKeepResultsInFindBox);
131 // Turn off hover state on close button else the button will remain
132 // hovered when we bring the find bar back up.
134 [[closeButton_ cell] setIsMouseInside:NO];
137 - (IBAction)previousResult:(id)sender {
138 if (findBarBridge_) {
139 FindTabHelper* findTabHelper = FindTabHelper::FromWebContents(
140 findBarBridge_->GetFindBarController()->web_contents());
141 findTabHelper->StartFinding(
142 base::SysNSStringToUTF16([findText_ stringValue]),
147 - (IBAction)nextResult:(id)sender {
148 if (findBarBridge_) {
149 FindTabHelper* findTabHelper = FindTabHelper::FromWebContents(
150 findBarBridge_->GetFindBarController()->web_contents());
151 findTabHelper->StartFinding(
152 base::SysNSStringToUTF16([findText_ stringValue]),
157 - (void)findPboardUpdated:(NSNotification*)notification {
158 if (!suppressPboardUpdateActions_)
159 [self prepopulateText:[[FindPasteboard sharedInstance] findText]];
160 [self clearFindResultsForCurrentBrowser];
163 - (void)positionFindBarViewAtMaxY:(CGFloat)maxY maxWidth:(CGFloat)maxWidth {
164 NSView* containerView = [self view];
165 CGFloat containerHeight = NSHeight([containerView frame]);
166 CGFloat containerWidth = std::min(maxWidth, defaultWidth_);
168 // Adjust where we'll actually place the find bar.
169 maxY += [containerView cr_lineWidth];
171 CGFloat x = [self findBarHorizontalPosition];
172 NSRect newFrame = NSMakeRect(x, maxY - containerHeight,
173 containerWidth, containerHeight);
175 if (moveAnimation_.get() != nil) {
176 NSRect frame = [containerView frame];
177 [moveAnimation_ stopAnimation];
178 // Restore to the X position before the animation was stopped. The Y
179 // position is immediately adjusted.
180 frame.origin.y = newFrame.origin.y;
181 [containerView setFrame:frame];
182 moveAnimation_.reset([self createAnimationForView:containerView
184 duration:kFindBarMoveDuration]);
186 [containerView setFrame:newFrame];
190 - (BOOL)isOffTheRecordProfile {
191 return browser_ && browser_->profile() &&
192 browser_->profile()->IsOffTheRecord();
195 // NSControl delegate method.
196 - (void)controlTextDidChange:(NSNotification*)aNotification {
200 content::WebContents* webContents =
201 findBarBridge_->GetFindBarController()->web_contents();
204 FindTabHelper* findTabHelper = FindTabHelper::FromWebContents(webContents);
206 NSString* findText = [findText_ stringValue];
207 if (![self isOffTheRecordProfile]) {
208 base::AutoReset<BOOL> suppressReset(&suppressPboardUpdateActions_, YES);
209 [[FindPasteboard sharedInstance] setFindText:findText];
212 if ([findText length] > 0) {
214 StartFinding(base::SysNSStringToUTF16(findText), true, false);
216 // The textbox is empty so we reset.
217 findTabHelper->StopFinding(FindBarController::kClearSelectionOnPage);
218 [self updateUIForFindResult:findTabHelper->find_result()
219 withText:base::string16()];
223 // NSControl delegate method
224 - (BOOL)control:(NSControl*)control
225 textView:(NSTextView*)textView
226 doCommandBySelector:(SEL)command {
227 if (command == @selector(insertNewline:)) {
229 NSEvent* event = [NSApp currentEvent];
231 if ([event modifierFlags] & NSShiftKeyMask)
232 [previousButton_ performClick:nil];
234 [nextButton_ performClick:nil];
237 } else if (command == @selector(insertLineBreak:)) {
238 // Pressing Ctrl-Return
239 if (findBarBridge_) {
240 findBarBridge_->GetFindBarController()->EndFindSession(
241 FindBarController::kActivateSelectionOnPage,
242 FindBarController::kClearResultsInFindBox);
245 } else if (command == @selector(pageUp:) ||
246 command == @selector(pageUpAndModifySelection:) ||
247 command == @selector(scrollPageUp:) ||
248 command == @selector(pageDown:) ||
249 command == @selector(pageDownAndModifySelection:) ||
250 command == @selector(scrollPageDown:) ||
251 command == @selector(scrollToBeginningOfDocument:) ||
252 command == @selector(scrollToEndOfDocument:) ||
253 command == @selector(moveUp:) ||
254 command == @selector(moveDown:)) {
255 content::WebContents* web_contents =
256 findBarBridge_->GetFindBarController()->web_contents();
260 // Sanity-check to make sure we got a keyboard event.
261 NSEvent* event = [NSApp currentEvent];
262 if ([event type] != NSKeyDown && [event type] != NSKeyUp)
265 // Forward the event to the renderer.
266 // TODO(rohitrao): Should this call -[BaseView keyEvent:]? Is there code in
267 // that function that we want to keep or avoid? Calling
268 // |ForwardKeyboardEvent()| directly ignores edit commands, which breaks
269 // cmd-up/down if we ever decide to include |moveToBeginningOfDocument:| in
271 content::RenderViewHost* render_view_host =
272 web_contents->GetRenderViewHost();
273 render_view_host->ForwardKeyboardEvent(NativeWebKeyboardEvent(event));
280 // Methods from FindBar
281 - (void)showFindBar:(BOOL)animate {
282 // Save the currently-focused view. |findBarView_| is in the view
283 // hierarchy by now. showFindBar can be called even when the
284 // findbar is already open, so do not overwrite an already saved
286 if (!focusTracker_.get())
288 [[FocusTracker alloc] initWithWindow:[findBarView_ window]]);
290 // The browser window might have changed while the FindBar was hidden.
291 // Update its position now.
292 [[self browserWindowController] layoutSubviews];
294 // Move to the correct horizontal position first, to prevent the FindBar
295 // from jumping around when switching tabs.
296 // Prevent jumping while the FindBar is animating (hiding, then showing) too.
297 if (![self isFindBarVisible])
298 [self moveFindBarIfNecessary:NO];
300 // Animate the view into place.
301 NSRect frame = [findBarView_ frame];
302 frame.origin = NSZeroPoint;
303 [self setFindBarFrame:frame animate:animate duration:kFindBarOpenDuration];
306 - (void)hideFindBar:(BOOL)animate {
307 NSRect frame = [self hiddenFindBarFrame];
308 [self setFindBarFrame:frame animate:animate duration:kFindBarCloseDuration];
311 - (void)stopAnimation {
312 if (showHideAnimation_.get()) {
313 [showHideAnimation_ stopAnimation];
314 showHideAnimation_.reset(nil);
316 if (moveAnimation_.get()) {
317 [moveAnimation_ stopAnimation];
318 moveAnimation_.reset(nil);
322 - (void)setFocusAndSelection {
323 [[findText_ window] makeFirstResponder:findText_];
325 // Enable the buttons if the find text is non-empty.
326 BOOL buttonsEnabled = ([[findText_ stringValue] length] > 0) ? YES : NO;
327 [previousButton_ setEnabled:buttonsEnabled];
328 [nextButton_ setEnabled:buttonsEnabled];
331 - (void)restoreSavedFocus {
332 if (!(focusTracker_.get() &&
333 [focusTracker_ restoreFocusInWindow:[findBarView_ window]])) {
334 // Fall back to giving focus to the tab contents.
335 findBarBridge_->GetFindBarController()->web_contents()->Focus();
337 focusTracker_.reset(nil);
340 - (void)setFindText:(NSString*)findText
341 selectedRange:(const NSRange&)selectedRange {
342 [findText_ setStringValue:findText];
344 if (![self isOffTheRecordProfile]) {
345 // Make sure the text in the find bar always ends up in the find pasteboard
346 // (and, via notifications, in the other find bars too).
347 base::AutoReset<BOOL> suppressReset(&suppressPboardUpdateActions_, YES);
348 [[FindPasteboard sharedInstance] setFindText:findText];
351 NSText* editor = [findText_ currentEditor];
352 if (selectedRange.location != NSNotFound)
353 [editor setSelectedRange:selectedRange];
356 - (NSString*)findText {
357 return [findText_ stringValue];
360 - (NSRange)selectedRange {
361 NSText* editor = [findText_ currentEditor];
362 return (editor != nil) ? [editor selectedRange] : NSMakeRange(NSNotFound, 0);
365 - (NSString*)matchCountText {
366 return [[findText_ findBarTextFieldCell] resultsString];
369 - (void)updateFindBarForChangedWebContents {
370 content::WebContents* contents =
371 findBarBridge_->GetFindBarController()->web_contents();
374 FindTabHelper* findTabHelper = FindTabHelper::FromWebContents(contents);
376 // If the find UI is visible but the results are cleared then also clear
377 // the results label and update the buttons.
378 if (findTabHelper->find_ui_active() &&
379 findTabHelper->previous_find_text().empty()) {
380 BOOL buttonsEnabled = [[findText_ stringValue] length] > 0 ? YES : NO;
381 [previousButton_ setEnabled:buttonsEnabled];
382 [nextButton_ setEnabled:buttonsEnabled];
383 [[findText_ findBarTextFieldCell] clearResults];
387 - (void)clearResults:(const FindNotificationDetails&)results {
388 // Just call updateUIForFindResult, which will take care of clearing
389 // the search text and the results label.
390 [self updateUIForFindResult:results withText:base::string16()];
393 - (void)updateUIForFindResult:(const FindNotificationDetails&)result
394 withText:(const base::string16&)findText {
395 // If we don't have any results and something was passed in, then
396 // that means someone pressed Cmd-G while the Find box was
397 // closed. In that case we need to repopulate the Find box with what
399 if ([[findText_ stringValue] length] == 0 && !findText.empty()) {
400 [findText_ setStringValue:base::SysUTF16ToNSString(findText)];
401 [findText_ selectText:self];
404 // Make sure Find Next and Find Previous are enabled if we found any matches.
405 BOOL buttonsEnabled = result.number_of_matches() > 0;
406 [previousButton_ setEnabled:buttonsEnabled];
407 [nextButton_ setEnabled:buttonsEnabled];
409 // Update the results label.
410 BOOL validRange = result.active_match_ordinal() != -1 &&
411 result.number_of_matches() != -1;
412 NSString* searchString = [findText_ stringValue];
413 if ([searchString length] > 0 && validRange) {
414 [[findText_ findBarTextFieldCell]
415 setActiveMatch:result.active_match_ordinal()
416 of:result.number_of_matches()];
418 // If there was no text entered, we don't show anything in the results area.
419 [[findText_ findBarTextFieldCell] clearResults];
422 [findText_ resetFieldEditorFrameIfNeeded];
424 // If we found any results, reset the focus tracker, so we always
425 // restore focus to the tab contents.
426 if (result.number_of_matches() > 0)
427 focusTracker_.reset(nil);
429 // Adjust the FindBar position, even when there are no matches (so that it
430 // goes back to the default position, if required).
431 [self moveFindBarIfNecessary:[self isFindBarVisible]];
434 - (BOOL)isFindBarVisible {
435 // Find bar is visible if any part of it is on the screen.
436 return NSIntersectsRect([[self view] bounds], [findBarView_ frame]);
439 - (BOOL)isFindBarAnimating {
440 return (showHideAnimation_.get() != nil) || (moveAnimation_.get() != nil);
443 // NSAnimation delegate methods.
444 - (void)animationDidEnd:(NSAnimation*)animation {
445 // Autorelease the animations (cannot use release because the animation object
446 // is still on the stack.
447 if (animation == showHideAnimation_.get()) {
448 [showHideAnimation_.release() autorelease];
449 } else if (animation == moveAnimation_.get()) {
450 [moveAnimation_.release() autorelease];
455 // If the find bar is not visible, make it actually hidden, so it'll no longer
456 // respond to key events.
457 [[self view] setHidden:![self isFindBarVisible]];
460 - (gfx::Point)findBarWindowPosition {
461 gfx::Rect viewRect(NSRectToCGRect([[self view] frame]));
462 // Convert Cocoa coordinates (Y growing up) to Y growing down.
463 // Offset from |maxY_|, which represents the content view's top, instead
464 // of from the superview, which represents the whole browser window.
465 viewRect.set_y(maxY_ - viewRect.bottom());
466 return viewRect.origin();
469 - (int)findBarWidth {
470 return NSWidth([[self view] frame]);
475 @implementation FindBarCocoaController (PrivateMethods)
477 - (NSRect)hiddenFindBarFrame {
478 NSRect frame = [findBarView_ frame];
479 NSRect containerBounds = [[self view] bounds];
480 frame.origin = NSMakePoint(NSMinX(containerBounds), NSMaxY(containerBounds));
484 - (NSViewAnimation*)createAnimationForView:(NSView*)view
485 toFrame:(NSRect)endFrame
486 duration:(float)duration {
487 NSDictionary* dict = [NSDictionary dictionaryWithObjectsAndKeys:
488 view, NSViewAnimationTargetKey,
489 [NSValue valueWithRect:endFrame], NSViewAnimationEndFrameKey, nil];
491 NSViewAnimation* animation =
492 [[NSViewAnimation alloc]
493 initWithViewAnimations:[NSArray arrayWithObjects:dict, nil]];
494 [animation gtm_setDuration:duration
495 eventMask:NSLeftMouseUpMask];
496 [animation setDelegate:self];
497 [animation startAnimation];
501 - (void)setFindBarFrame:(NSRect)endFrame
502 animate:(BOOL)animate
503 duration:(float)duration {
504 // Save the current frame.
505 NSRect startFrame = [findBarView_ frame];
507 // Stop any existing animations.
508 [showHideAnimation_ stopAnimation];
511 [findBarView_ setFrame:endFrame];
512 [[self view] setHidden:![self isFindBarVisible]];
513 showHideAnimation_.reset(nil);
517 // If animating, ensure that the find bar is not hidden. Hidden status will be
518 // updated at the end of the animation.
519 [[self view] setHidden:NO];
521 // Reset the frame to what was saved above.
522 [findBarView_ setFrame:startFrame];
524 showHideAnimation_.reset([self createAnimationForView:findBarView_
529 - (float)findBarHorizontalPosition {
530 // Get the rect of the FindBar.
531 NSView* view = [self view];
532 NSRect frame = [view frame];
533 gfx::Rect viewRect(NSRectToCGRect(frame));
535 if (!findBarBridge_ || !findBarBridge_->GetFindBarController())
536 return frame.origin.x;
537 content::WebContents* contents =
538 findBarBridge_->GetFindBarController()->web_contents();
540 return frame.origin.x;
542 // Get the size of the container.
543 gfx::Rect containerRect(contents->GetContainerBounds().size());
545 // Position the FindBar on the top right corner.
547 containerRect.width() - viewRect.width() - kRightEdgeOffset);
548 // Convert from Cocoa coordinates (Y growing up) to Y growing down.
549 // Notice that the view frame's Y offset is relative to the whole window,
550 // while GetLocationForFindbarView() expects it relative to the
551 // content's boundaries. |maxY_| has the correct placement in Cocoa coords,
552 // so we just have to invert the Y coordinate.
553 viewRect.set_y(maxY_ - viewRect.bottom());
555 // Get the rect of the current find result, if there is one.
556 const FindNotificationDetails& findResult =
557 FindTabHelper::FromWebContents(contents)->find_result();
558 if (findResult.number_of_matches() == 0)
560 gfx::Rect selectionRect(findResult.selection_rect());
562 // Adjust |view_rect| to avoid the |selection_rect| within |container_rect|.
563 gfx::Rect newPos = FindBarController::GetLocationForFindbarView(
564 viewRect, containerRect, selectionRect);
569 - (void)moveFindBarIfNecessary:(BOOL)animate {
570 // Don't animate during tests.
571 if (FindBarBridge::disable_animations_during_testing_)
574 NSView* view = [self view];
575 NSRect frame = [view frame];
576 float x = [self findBarHorizontalPosition];
577 if (frame.origin.x == x)
581 [moveAnimation_ stopAnimation];
582 // Restore to the position before the animation was stopped.
583 [view setFrame:frame];
585 moveAnimation_.reset([self createAnimationForView:view
587 duration:kFindBarMoveDuration]);
590 [view setFrame:frame];
594 - (void)prepopulateText:(NSString*)text {
595 [self setFindText:text selectedRange:NSMakeRange(NSNotFound, 0)];
597 // Has to happen after |ClearResults()| above.
598 BOOL buttonsEnabled = [text length] > 0 ? YES : NO;
599 [previousButton_ setEnabled:buttonsEnabled];
600 [nextButton_ setEnabled:buttonsEnabled];
603 - (void)clearFindResultsForCurrentBrowser {
607 content::WebContents* activeWebContents =
608 findBarBridge_->GetFindBarController()->web_contents();
610 TabStripModel* tabStripModel = browser_->tab_strip_model();
611 for (int i = 0; i < tabStripModel->count(); ++i) {
612 content::WebContents* webContents = tabStripModel->GetWebContentsAt(i);
613 if (suppressPboardUpdateActions_ && activeWebContents == webContents)
615 FindTabHelper* findTabHelper =
616 FindTabHelper::FromWebContents(webContents);
617 findTabHelper->StopFinding(FindBarController::kClearSelectionOnPage);
618 findBarBridge_->ClearResults(findTabHelper->find_result());
622 - (BrowserWindowController*)browserWindowController {
625 return [BrowserWindowController
626 browserWindowControllerForWindow:browser_->window()->GetNativeWindow()];