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/mac/mac_util.h"
10 #include "base/strings/sys_string_conversions.h"
11 #include "grit/theme_resources.h"
12 #include "grit/ui_resources.h"
13 #include "chrome/browser/ui/browser_finder.h"
14 #include "chrome/browser/ui/browser_window.h"
15 #include "chrome/browser/ui/cocoa/browser_window_controller.h"
16 #import "chrome/browser/ui/cocoa/find_bar/find_bar_bridge.h"
17 #import "chrome/browser/ui/cocoa/find_bar/find_bar_cocoa_controller.h"
18 #import "chrome/browser/ui/cocoa/find_bar/find_bar_text_field.h"
19 #import "chrome/browser/ui/cocoa/find_bar/find_bar_text_field_cell.h"
20 #import "chrome/browser/ui/cocoa/image_button_cell.h"
21 #import "chrome/browser/ui/cocoa/nsview_additions.h"
22 #import "chrome/browser/ui/cocoa/tabs/tab_strip_controller.h"
23 #include "chrome/browser/ui/find_bar/find_bar_controller.h"
24 #include "chrome/browser/ui/find_bar/find_tab_helper.h"
25 #include "content/public/browser/render_view_host.h"
26 #include "content/public/browser/web_contents.h"
27 #include "content/public/browser/web_contents_view.h"
28 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSAnimation+Duration.h"
29 #import "ui/base/cocoa/find_pasteboard.h"
30 #import "ui/base/cocoa/focus_tracker.h"
32 using content::NativeWebKeyboardEvent;
34 const float kFindBarOpenDuration = 0.2;
35 const float kFindBarCloseDuration = 0.15;
36 const float kFindBarMoveDuration = 0.15;
37 const float kRightEdgeOffset = 25;
40 @interface FindBarCocoaController (PrivateMethods) <NSAnimationDelegate>
41 // Returns the appropriate frame for a hidden find bar.
42 - (NSRect)hiddenFindBarFrame;
44 // Animates the given |view| to the given |endFrame| within |duration| seconds.
45 // Returns a new NSViewAnimation.
46 - (NSViewAnimation*)createAnimationForView:(NSView*)view
47 toFrame:(NSRect)endFrame
48 duration:(float)duration;
50 // Sets the frame of |findBarView_|. |duration| is ignored if |animate| is NO.
51 - (void)setFindBarFrame:(NSRect)endFrame
53 duration:(float)duration;
55 // Returns the horizontal position the FindBar should use in order to avoid
56 // overlapping with the current find result, if there's one.
57 - (float)findBarHorizontalPosition;
59 // Adjusts the horizontal position if necessary to avoid overlapping with the
60 // current find result.
61 - (void)moveFindBarIfNecessary:(BOOL)animate;
63 // Puts |text| into the find bar and enables the buttons, but doesn't start a
64 // new search for |text|.
65 - (void)prepopulateText:(NSString*)text;
67 // Clears the find results for all tabs in browser associated with this find
68 // bar. If |suppressPboardUpdateActions_| is true then the current tab is not
70 - (void)clearFindResultsForCurrentBrowser;
72 - (BrowserWindowController*)browserWindowController;
75 @implementation FindBarCocoaController
77 @synthesize findBarView = findBarView_;
79 - (id)initWithBrowser:(Browser*)browser {
80 if ((self = [super initWithNibName:@"FindBar"
81 bundle:base::mac::FrameworkBundle()])) {
82 [[NSNotificationCenter defaultCenter]
84 selector:@selector(findPboardUpdated:)
85 name:kFindPasteboardChangedNotification
86 object:[FindPasteboard sharedInstance]];
93 // All animations should have been explicitly stopped before a tab is closed.
94 DCHECK(!showHideAnimation_.get());
95 DCHECK(!moveAnimation_.get());
96 [[NSNotificationCenter defaultCenter] removeObserver:self];
100 - (void)setFindBarBridge:(FindBarBridge*)findBarBridge {
101 DCHECK(!findBarBridge_); // should only be called once.
102 findBarBridge_ = findBarBridge;
105 - (void)awakeFromNib {
106 [[closeButton_ cell] setImageID:IDR_CLOSE_1
107 forButtonState:image_button_cell::kDefaultState];
108 [[closeButton_ cell] setImageID:IDR_CLOSE_1_H
109 forButtonState:image_button_cell::kHoverState];
110 [[closeButton_ cell] setImageID:IDR_CLOSE_1_P
111 forButtonState:image_button_cell::kPressedState];
112 [[closeButton_ cell] setImageID:IDR_CLOSE_1
113 forButtonState:image_button_cell::kDisabledState];
115 [findBarView_ setFrame:[self hiddenFindBarFrame]];
116 defaultWidth_ = NSWidth([findBarView_ frame]);
118 [self prepopulateText:[[FindPasteboard sharedInstance] findText]];
121 - (IBAction)close:(id)sender {
123 findBarBridge_->GetFindBarController()->EndFindSession(
124 FindBarController::kKeepSelectionOnPage,
125 FindBarController::kKeepResultsInFindBox);
127 // Turn off hover state on close button else the button will remain
128 // hovered when we bring the find bar back up.
130 [[closeButton_ cell] setIsMouseInside:NO];
133 - (IBAction)previousResult:(id)sender {
134 if (findBarBridge_) {
135 FindTabHelper* findTabHelper = FindTabHelper::FromWebContents(
136 findBarBridge_->GetFindBarController()->web_contents());
137 findTabHelper->StartFinding(
138 base::SysNSStringToUTF16([findText_ stringValue]),
143 - (IBAction)nextResult:(id)sender {
144 if (findBarBridge_) {
145 FindTabHelper* findTabHelper = FindTabHelper::FromWebContents(
146 findBarBridge_->GetFindBarController()->web_contents());
147 findTabHelper->StartFinding(
148 base::SysNSStringToUTF16([findText_ stringValue]),
153 - (void)findPboardUpdated:(NSNotification*)notification {
154 if (!suppressPboardUpdateActions_)
155 [self prepopulateText:[[FindPasteboard sharedInstance] findText]];
156 [self clearFindResultsForCurrentBrowser];
159 - (void)positionFindBarViewAtMaxY:(CGFloat)maxY maxWidth:(CGFloat)maxWidth {
160 NSView* containerView = [self view];
161 CGFloat containerHeight = NSHeight([containerView frame]);
162 CGFloat containerWidth = std::min(maxWidth, defaultWidth_);
164 // Adjust where we'll actually place the find bar.
165 maxY += [containerView cr_lineWidth];
167 CGFloat x = [self findBarHorizontalPosition];
168 NSRect newFrame = NSMakeRect(x, maxY - containerHeight,
169 containerWidth, containerHeight);
171 if (moveAnimation_.get() != nil) {
172 NSRect frame = [containerView frame];
173 [moveAnimation_ stopAnimation];
174 // Restore to the X position before the animation was stopped. The Y
175 // position is immediately adjusted.
176 frame.origin.y = newFrame.origin.y;
177 [containerView setFrame:frame];
178 moveAnimation_.reset([self createAnimationForView:containerView
180 duration:kFindBarMoveDuration]);
182 [containerView setFrame:newFrame];
186 - (BOOL)isOffTheRecordProfile {
187 return browser_ && browser_->profile() &&
188 browser_->profile()->IsOffTheRecord();
191 // NSControl delegate method.
192 - (void)controlTextDidChange:(NSNotification*)aNotification {
196 content::WebContents* webContents =
197 findBarBridge_->GetFindBarController()->web_contents();
200 FindTabHelper* findTabHelper = FindTabHelper::FromWebContents(webContents);
202 NSString* findText = [findText_ stringValue];
203 if (![self isOffTheRecordProfile]) {
204 base::AutoReset<BOOL> suppressReset(&suppressPboardUpdateActions_, YES);
205 [[FindPasteboard sharedInstance] setFindText:findText];
208 if ([findText length] > 0) {
210 StartFinding(base::SysNSStringToUTF16(findText), true, false);
212 // The textbox is empty so we reset.
213 findTabHelper->StopFinding(FindBarController::kClearSelectionOnPage);
214 [self updateUIForFindResult:findTabHelper->find_result()
215 withText:base::string16()];
219 // NSControl delegate method
220 - (BOOL)control:(NSControl*)control
221 textView:(NSTextView*)textView
222 doCommandBySelector:(SEL)command {
223 if (command == @selector(insertNewline:)) {
225 NSEvent* event = [NSApp currentEvent];
227 if ([event modifierFlags] & NSShiftKeyMask)
228 [previousButton_ performClick:nil];
230 [nextButton_ performClick:nil];
233 } else if (command == @selector(insertLineBreak:)) {
234 // Pressing Ctrl-Return
235 if (findBarBridge_) {
236 findBarBridge_->GetFindBarController()->EndFindSession(
237 FindBarController::kActivateSelectionOnPage,
238 FindBarController::kClearResultsInFindBox);
241 } else if (command == @selector(pageUp:) ||
242 command == @selector(pageUpAndModifySelection:) ||
243 command == @selector(scrollPageUp:) ||
244 command == @selector(pageDown:) ||
245 command == @selector(pageDownAndModifySelection:) ||
246 command == @selector(scrollPageDown:) ||
247 command == @selector(scrollToBeginningOfDocument:) ||
248 command == @selector(scrollToEndOfDocument:) ||
249 command == @selector(moveUp:) ||
250 command == @selector(moveDown:)) {
251 content::WebContents* web_contents =
252 findBarBridge_->GetFindBarController()->web_contents();
256 // Sanity-check to make sure we got a keyboard event.
257 NSEvent* event = [NSApp currentEvent];
258 if ([event type] != NSKeyDown && [event type] != NSKeyUp)
261 // Forward the event to the renderer.
262 // TODO(rohitrao): Should this call -[BaseView keyEvent:]? Is there code in
263 // that function that we want to keep or avoid? Calling
264 // |ForwardKeyboardEvent()| directly ignores edit commands, which breaks
265 // cmd-up/down if we ever decide to include |moveToBeginningOfDocument:| in
267 content::RenderViewHost* render_view_host =
268 web_contents->GetRenderViewHost();
269 render_view_host->ForwardKeyboardEvent(NativeWebKeyboardEvent(event));
276 // Methods from FindBar
277 - (void)showFindBar:(BOOL)animate {
278 // Save the currently-focused view. |findBarView_| is in the view
279 // hierarchy by now. showFindBar can be called even when the
280 // findbar is already open, so do not overwrite an already saved
282 if (!focusTracker_.get())
284 [[FocusTracker alloc] initWithWindow:[findBarView_ window]]);
286 // The browser window might have changed while the FindBar was hidden.
287 // Update its position now.
288 [[self browserWindowController] layoutSubviews];
290 // Move to the correct horizontal position first, to prevent the FindBar
291 // from jumping around when switching tabs.
292 // Prevent jumping while the FindBar is animating (hiding, then showing) too.
293 if (![self isFindBarVisible])
294 [self moveFindBarIfNecessary:NO];
296 // Animate the view into place.
297 NSRect frame = [findBarView_ frame];
298 frame.origin = NSZeroPoint;
299 [self setFindBarFrame:frame animate:animate duration:kFindBarOpenDuration];
302 - (void)hideFindBar:(BOOL)animate {
303 NSRect frame = [self hiddenFindBarFrame];
304 [self setFindBarFrame:frame animate:animate duration:kFindBarCloseDuration];
307 - (void)stopAnimation {
308 if (showHideAnimation_.get()) {
309 [showHideAnimation_ stopAnimation];
310 showHideAnimation_.reset(nil);
312 if (moveAnimation_.get()) {
313 [moveAnimation_ stopAnimation];
314 moveAnimation_.reset(nil);
318 - (void)setFocusAndSelection {
319 [[findText_ window] makeFirstResponder:findText_];
321 // Enable the buttons if the find text is non-empty.
322 BOOL buttonsEnabled = ([[findText_ stringValue] length] > 0) ? YES : NO;
323 [previousButton_ setEnabled:buttonsEnabled];
324 [nextButton_ setEnabled:buttonsEnabled];
327 - (void)restoreSavedFocus {
328 if (!(focusTracker_.get() &&
329 [focusTracker_ restoreFocusInWindow:[findBarView_ window]])) {
330 // Fall back to giving focus to the tab contents.
331 findBarBridge_->GetFindBarController()->web_contents()->GetView()->Focus();
333 focusTracker_.reset(nil);
336 - (void)setFindText:(NSString*)findText
337 selectedRange:(const NSRange&)selectedRange {
338 [findText_ setStringValue:findText];
340 if (![self isOffTheRecordProfile]) {
341 // Make sure the text in the find bar always ends up in the find pasteboard
342 // (and, via notifications, in the other find bars too).
343 base::AutoReset<BOOL> suppressReset(&suppressPboardUpdateActions_, YES);
344 [[FindPasteboard sharedInstance] setFindText:findText];
347 NSText* editor = [findText_ currentEditor];
348 if (selectedRange.location != NSNotFound)
349 [editor setSelectedRange:selectedRange];
352 - (NSString*)findText {
353 return [findText_ stringValue];
356 - (NSRange)selectedRange {
357 NSText* editor = [findText_ currentEditor];
358 return (editor != nil) ? [editor selectedRange] : NSMakeRange(NSNotFound, 0);
361 - (NSString*)matchCountText {
362 return [[findText_ findBarTextFieldCell] resultsString];
365 - (void)updateFindBarForChangedWebContents {
366 content::WebContents* contents =
367 findBarBridge_->GetFindBarController()->web_contents();
370 FindTabHelper* findTabHelper = FindTabHelper::FromWebContents(contents);
372 // If the find UI is visible but the results are cleared then also clear
373 // the results label and update the buttons.
374 if (findTabHelper->find_ui_active() &&
375 findTabHelper->previous_find_text().empty()) {
376 BOOL buttonsEnabled = [[findText_ stringValue] length] > 0 ? YES : NO;
377 [previousButton_ setEnabled:buttonsEnabled];
378 [nextButton_ setEnabled:buttonsEnabled];
379 [[findText_ findBarTextFieldCell] clearResults];
383 - (void)clearResults:(const FindNotificationDetails&)results {
384 // Just call updateUIForFindResult, which will take care of clearing
385 // the search text and the results label.
386 [self updateUIForFindResult:results withText:base::string16()];
389 - (void)updateUIForFindResult:(const FindNotificationDetails&)result
390 withText:(const base::string16&)findText {
391 // If we don't have any results and something was passed in, then
392 // that means someone pressed Cmd-G while the Find box was
393 // closed. In that case we need to repopulate the Find box with what
395 if ([[findText_ stringValue] length] == 0 && !findText.empty()) {
396 [findText_ setStringValue:base::SysUTF16ToNSString(findText)];
397 [findText_ selectText:self];
400 // Make sure Find Next and Find Previous are enabled if we found any matches.
401 BOOL buttonsEnabled = result.number_of_matches() > 0;
402 [previousButton_ setEnabled:buttonsEnabled];
403 [nextButton_ setEnabled:buttonsEnabled];
405 // Update the results label.
406 BOOL validRange = result.active_match_ordinal() != -1 &&
407 result.number_of_matches() != -1;
408 NSString* searchString = [findText_ stringValue];
409 if ([searchString length] > 0 && validRange) {
410 [[findText_ findBarTextFieldCell]
411 setActiveMatch:result.active_match_ordinal()
412 of:result.number_of_matches()];
414 // If there was no text entered, we don't show anything in the results area.
415 [[findText_ findBarTextFieldCell] clearResults];
418 [findText_ resetFieldEditorFrameIfNeeded];
420 // If we found any results, reset the focus tracker, so we always
421 // restore focus to the tab contents.
422 if (result.number_of_matches() > 0)
423 focusTracker_.reset(nil);
425 // Adjust the FindBar position, even when there are no matches (so that it
426 // goes back to the default position, if required).
427 [self moveFindBarIfNecessary:[self isFindBarVisible]];
430 - (BOOL)isFindBarVisible {
431 // Find bar is visible if any part of it is on the screen.
432 return NSIntersectsRect([[self view] bounds], [findBarView_ frame]);
435 - (BOOL)isFindBarAnimating {
436 return (showHideAnimation_.get() != nil) || (moveAnimation_.get() != nil);
439 // NSAnimation delegate methods.
440 - (void)animationDidEnd:(NSAnimation*)animation {
441 // Autorelease the animations (cannot use release because the animation object
442 // is still on the stack.
443 if (animation == showHideAnimation_.get()) {
444 [showHideAnimation_.release() autorelease];
445 } else if (animation == moveAnimation_.get()) {
446 [moveAnimation_.release() autorelease];
451 // If the find bar is not visible, make it actually hidden, so it'll no longer
452 // respond to key events.
453 [findBarView_ setHidden:![self isFindBarVisible]];
454 [[self browserWindowController] onFindBarVisibilityChanged];
457 - (gfx::Point)findBarWindowPosition {
458 gfx::Rect viewRect(NSRectToCGRect([[self view] frame]));
459 // Convert Cocoa coordinates (Y growing up) to Y growing down.
460 // Offset from |maxY_|, which represents the content view's top, instead
461 // of from the superview, which represents the whole browser window.
462 viewRect.set_y(maxY_ - viewRect.bottom());
463 return viewRect.origin();
466 - (int)findBarWidth {
467 return NSWidth([[self view] frame]);
472 @implementation FindBarCocoaController (PrivateMethods)
474 - (NSRect)hiddenFindBarFrame {
475 NSRect frame = [findBarView_ frame];
476 NSRect containerBounds = [[self view] bounds];
477 frame.origin = NSMakePoint(NSMinX(containerBounds), NSMaxY(containerBounds));
481 - (NSViewAnimation*)createAnimationForView:(NSView*)view
482 toFrame:(NSRect)endFrame
483 duration:(float)duration {
484 NSDictionary* dict = [NSDictionary dictionaryWithObjectsAndKeys:
485 view, NSViewAnimationTargetKey,
486 [NSValue valueWithRect:endFrame], NSViewAnimationEndFrameKey, nil];
488 NSViewAnimation* animation =
489 [[NSViewAnimation alloc]
490 initWithViewAnimations:[NSArray arrayWithObjects:dict, nil]];
491 [animation gtm_setDuration:duration
492 eventMask:NSLeftMouseUpMask];
493 [animation setDelegate:self];
494 [animation startAnimation];
498 - (void)setFindBarFrame:(NSRect)endFrame
499 animate:(BOOL)animate
500 duration:(float)duration {
501 // Save the current frame.
502 NSRect startFrame = [findBarView_ frame];
504 // Stop any existing animations.
505 [showHideAnimation_ stopAnimation];
508 [findBarView_ setFrame:endFrame];
509 [findBarView_ setHidden:![self isFindBarVisible]];
510 [[self browserWindowController] onFindBarVisibilityChanged];
511 showHideAnimation_.reset(nil);
515 // If animating, ensure that the find bar is not hidden. Hidden status will be
516 // updated at the end of the animation.
517 [findBarView_ setHidden:NO];
518 //[[self browserWindowController] onFindBarVisibilityChanged];
520 // Reset the frame to what was saved above.
521 [findBarView_ setFrame:startFrame];
523 [[self browserWindowController] onFindBarVisibilityChanged];
525 showHideAnimation_.reset([self createAnimationForView:findBarView_
530 - (float)findBarHorizontalPosition {
531 // Get the rect of the FindBar.
532 NSView* view = [self view];
533 NSRect frame = [view frame];
534 gfx::Rect viewRect(NSRectToCGRect(frame));
536 if (!findBarBridge_ || !findBarBridge_->GetFindBarController())
537 return frame.origin.x;
538 content::WebContents* contents =
539 findBarBridge_->GetFindBarController()->web_contents();
541 return frame.origin.x;
543 // Get the size of the container.
544 gfx::Rect containerRect(contents->GetView()->GetContainerSize());
546 // Position the FindBar on the top right corner.
548 containerRect.width() - viewRect.width() - kRightEdgeOffset);
549 // Convert from Cocoa coordinates (Y growing up) to Y growing down.
550 // Notice that the view frame's Y offset is relative to the whole window,
551 // while GetLocationForFindbarView() expects it relative to the
552 // content's boundaries. |maxY_| has the correct placement in Cocoa coords,
553 // so we just have to invert the Y coordinate.
554 viewRect.set_y(maxY_ - viewRect.bottom());
556 // Get the rect of the current find result, if there is one.
557 const FindNotificationDetails& findResult =
558 FindTabHelper::FromWebContents(contents)->find_result();
559 if (findResult.number_of_matches() == 0)
561 gfx::Rect selectionRect(findResult.selection_rect());
563 // Adjust |view_rect| to avoid the |selection_rect| within |container_rect|.
564 gfx::Rect newPos = FindBarController::GetLocationForFindbarView(
565 viewRect, containerRect, selectionRect);
570 - (void)moveFindBarIfNecessary:(BOOL)animate {
571 // Don't animate during tests.
572 if (FindBarBridge::disable_animations_during_testing_)
575 NSView* view = [self view];
576 NSRect frame = [view frame];
577 float x = [self findBarHorizontalPosition];
578 if (frame.origin.x == x)
582 [moveAnimation_ stopAnimation];
583 // Restore to the position before the animation was stopped.
584 [view setFrame:frame];
586 moveAnimation_.reset([self createAnimationForView:view
588 duration:kFindBarMoveDuration]);
591 [view setFrame:frame];
595 - (void)prepopulateText:(NSString*)text {
596 [self setFindText:text selectedRange:NSMakeRange(NSNotFound, 0)];
598 // Has to happen after |ClearResults()| above.
599 BOOL buttonsEnabled = [text length] > 0 ? YES : NO;
600 [previousButton_ setEnabled:buttonsEnabled];
601 [nextButton_ setEnabled:buttonsEnabled];
604 - (void)clearFindResultsForCurrentBrowser {
608 content::WebContents* activeWebContents =
609 findBarBridge_->GetFindBarController()->web_contents();
611 TabStripModel* tabStripModel = browser_->tab_strip_model();
612 for (int i = 0; i < tabStripModel->count(); ++i) {
613 content::WebContents* webContents = tabStripModel->GetWebContentsAt(i);
614 if (suppressPboardUpdateActions_ && activeWebContents == webContents)
616 FindTabHelper* findTabHelper =
617 FindTabHelper::FromWebContents(webContents);
618 findTabHelper->StopFinding(FindBarController::kClearSelectionOnPage);
619 findBarBridge_->ClearResults(findTabHelper->find_result());
623 - (BrowserWindowController*)browserWindowController {
626 return [BrowserWindowController
627 browserWindowControllerForWindow:browser_->window()->GetNativeWindow()];