Popular sites on the NTP: check that experiment group StartsWith (rather than IS...
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / find_bar / find_bar_cocoa_controller.mm
blob795415f90a5d7515e406fcfa995c3b378ffe679a
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
50                 animate:(BOOL)animate
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
67 // cleared.
68 - (void)clearFindResultsForCurrentBrowser;
70 - (BrowserWindowController*)browserWindowController;
71 @end
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]
81         addObserver:self
82            selector:@selector(findPboardUpdated:)
83                name:kFindPasteboardChangedNotification
84              object:[FindPasteboard sharedInstance]];
85     browser_ = browser;
86   }
87   return self;
90 - (void)dealloc {
91   [self browserWillBeDestroyed];
92   [super dealloc];
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];
100   browser_ = nullptr;
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 {
126   if (findBarBridge_)
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.
133   // crbug.com/227424
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]),
143         false, false);
144   }
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]),
153         true, false);
154   }
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];
170   maxY_ = maxY;
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
183                                               toFrame:newFrame
184                                              duration:kFindBarMoveDuration]);
185   } else {
186     [containerView setFrame:newFrame];
187   }
190 - (BOOL)isOffTheRecordProfile {
191   return browser_ && browser_->profile() &&
192          browser_->profile()->IsOffTheRecord();
195 // NSControl delegate method.
196 - (void)controlTextDidChange:(NSNotification*)aNotification {
197   if (!findBarBridge_)
198     return;
200   content::WebContents* webContents =
201       findBarBridge_->GetFindBarController()->web_contents();
202   if (!webContents)
203     return;
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];
210   }
212   if ([findText length] > 0) {
213     findTabHelper->
214         StartFinding(base::SysNSStringToUTF16(findText), true, false);
215   } else {
216     // The textbox is empty so we reset.
217     findTabHelper->StopFinding(FindBarController::kClearSelectionOnPage);
218     [self updateUIForFindResult:findTabHelper->find_result()
219                        withText:base::string16()];
220   }
223 // NSControl delegate method
224 - (BOOL)control:(NSControl*)control
225     textView:(NSTextView*)textView
226     doCommandBySelector:(SEL)command {
227   if (command == @selector(insertNewline:)) {
228     // Pressing Return
229     NSEvent* event = [NSApp currentEvent];
231     if ([event modifierFlags] & NSShiftKeyMask)
232       [previousButton_ performClick:nil];
233     else
234       [nextButton_ performClick:nil];
236     return YES;
237   } else if (command == @selector(insertLineBreak:)) {
238     // Pressing Ctrl-Return
239     if (findBarBridge_) {
240       findBarBridge_->GetFindBarController()->EndFindSession(
241           FindBarController::kActivateSelectionOnPage,
242           FindBarController::kClearResultsInFindBox);
243     }
244     return YES;
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();
257     if (!web_contents)
258       return NO;
260     // Sanity-check to make sure we got a keyboard event.
261     NSEvent* event = [NSApp currentEvent];
262     if ([event type] != NSKeyDown && [event type] != NSKeyUp)
263       return NO;
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
270     // the list above.
271     content::RenderViewHost* render_view_host =
272         web_contents->GetRenderViewHost();
273     render_view_host->ForwardKeyboardEvent(NativeWebKeyboardEvent(event));
274     return YES;
275   }
277   return NO;
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
285   // view.
286   if (!focusTracker_.get())
287     focusTracker_.reset(
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);
315   }
316   if (moveAnimation_.get()) {
317     [moveAnimation_ stopAnimation];
318     moveAnimation_.reset(nil);
319   }
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();
336   }
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];
349   }
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();
372   if (!contents)
373     return;
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];
384   }
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
398   // was passed in.
399   if ([[findText_ stringValue] length] == 0 && !findText.empty()) {
400     [findText_ setStringValue:base::SysUTF16ToNSString(findText)];
401     [findText_ selectText:self];
402   }
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()];
417   } else {
418     // If there was no text entered, we don't show anything in the results area.
419     [[findText_ findBarTextFieldCell] clearResults];
420   }
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];
451   } else {
452     NOTREACHED();
453   }
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]);
473 @end
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));
481   return frame;
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];
498   return animation;
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];
510   if (!animate) {
511     [findBarView_ setFrame:endFrame];
512     [[self view] setHidden:![self isFindBarVisible]];
513     showHideAnimation_.reset(nil);
514     return;
515   }
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_
525                                                 toFrame:endFrame
526                                                duration:duration]);
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();
539   if (!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.
546   viewRect.set_x(
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)
559     return viewRect.x();
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);
566   return newPos.x();
569 - (void)moveFindBarIfNecessary:(BOOL)animate {
570   // Don't animate during tests.
571   if (FindBarBridge::disable_animations_during_testing_)
572     animate = NO;
574   NSView* view = [self view];
575   NSRect frame = [view frame];
576   float x = [self findBarHorizontalPosition];
577   if (frame.origin.x == x)
578     return;
580   if (animate) {
581     [moveAnimation_ stopAnimation];
582     // Restore to the position before the animation was stopped.
583     [view setFrame:frame];
584     frame.origin.x = x;
585     moveAnimation_.reset([self createAnimationForView:view
586                                               toFrame:frame
587                                              duration:kFindBarMoveDuration]);
588   } else {
589     frame.origin.x = x;
590     [view setFrame:frame];
591   }
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 {
604   if (!browser_)
605     return;
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)
614       continue;
615     FindTabHelper* findTabHelper =
616         FindTabHelper::FromWebContents(webContents);
617     findTabHelper->StopFinding(FindBarController::kClearSelectionOnPage);
618     findBarBridge_->ClearResults(findTabHelper->find_result());
619   }
622 - (BrowserWindowController*)browserWindowController {
623   if (!browser_)
624     return nil;
625   return [BrowserWindowController
626       browserWindowControllerForWindow:browser_->window()->GetNativeWindow()];
629 @end