• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
26const float kFindBarOpenDuration = 0.2;
27const float kFindBarCloseDuration = 0.15;
28const float kFindBarMoveDuration = 0.15;
29const 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