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.
4
5#include "chrome/browser/ui/find_bar/find_bar_controller.h"
6
7#include <algorithm>
8
9#include "base/i18n/rtl.h"
10#include "base/logging.h"
11#include "build/build_config.h"
12#include "chrome/browser/chrome_notification_types.h"
13#include "chrome/browser/profiles/profile.h"
14#include "chrome/browser/ui/find_bar/find_bar.h"
15#include "chrome/browser/ui/find_bar/find_bar_state.h"
16#include "chrome/browser/ui/find_bar/find_bar_state_factory.h"
17#include "chrome/browser/ui/find_bar/find_tab_helper.h"
18#include "content/public/browser/navigation_details.h"
19#include "content/public/browser/navigation_entry.h"
20#include "content/public/browser/notification_details.h"
21#include "content/public/browser/notification_source.h"
22#include "content/public/browser/web_contents.h"
23#include "ui/gfx/rect.h"
24
25using content::NavigationController;
26using content::WebContents;
27
28// The minimum space between the FindInPage window and the search result.
29static const int kMinFindWndDistanceFromSelection = 5;
30
31FindBarController::FindBarController(FindBar* find_bar)
32    : find_bar_(find_bar),
33      web_contents_(NULL),
34      last_reported_matchcount_(0) {
35}
36
37FindBarController::~FindBarController() {
38  DCHECK(!web_contents_);
39}
40
41void FindBarController::Show() {
42  FindTabHelper* find_tab_helper =
43      FindTabHelper::FromWebContents(web_contents_);
44
45  // Only show the animation if we're not already showing a find bar for the
46  // selected WebContents.
47  if (!find_tab_helper->find_ui_active()) {
48    MaybeSetPrepopulateText();
49
50    find_tab_helper->set_find_ui_active(true);
51    find_bar_->Show(true);
52  }
53  find_bar_->SetFocusAndSelection();
54}
55
56void FindBarController::EndFindSession(SelectionAction selection_action,
57                                       ResultAction result_action) {
58  find_bar_->Hide(true);
59
60  // |web_contents_| can be NULL for a number of reasons, for example when the
61  // tab is closing. We must guard against that case. See issue 8030.
62  if (web_contents_) {
63    FindTabHelper* find_tab_helper =
64        FindTabHelper::FromWebContents(web_contents_);
65
66    // When we hide the window, we need to notify the renderer that we are done
67    // for now, so that we can abort the scoping effort and clear all the
68    // tickmarks and highlighting.
69    find_tab_helper->StopFinding(selection_action);
70
71    if (result_action == kClearResultsInFindBox)
72      find_bar_->ClearResults(find_tab_helper->find_result());
73
74    // When we get dismissed we restore the focus to where it belongs.
75    find_bar_->RestoreSavedFocus();
76  }
77}
78
79void FindBarController::ChangeWebContents(WebContents* contents) {
80  if (web_contents_) {
81    registrar_.RemoveAll();
82    find_bar_->StopAnimation();
83
84    FindTabHelper* find_tab_helper =
85        FindTabHelper::FromWebContents(web_contents_);
86    if (find_tab_helper)
87      find_tab_helper->set_selected_range(find_bar_->GetSelectedRange());
88  }
89
90  web_contents_ = contents;
91  FindTabHelper* find_tab_helper =
92      web_contents_ ? FindTabHelper::FromWebContents(web_contents_)
93                    : NULL;
94
95  // Hide any visible find window from the previous tab if a NULL tab contents
96  // is passed in or if the find UI is not active in the new tab.
97  if (find_bar_->IsFindBarVisible() &&
98      (!find_tab_helper || !find_tab_helper->find_ui_active())) {
99    find_bar_->Hide(false);
100  }
101
102  if (!web_contents_)
103    return;
104
105  registrar_.Add(this,
106                 chrome::NOTIFICATION_FIND_RESULT_AVAILABLE,
107                 content::Source<WebContents>(web_contents_));
108  registrar_.Add(
109      this,
110      content::NOTIFICATION_NAV_ENTRY_COMMITTED,
111      content::Source<NavigationController>(&web_contents_->GetController()));
112
113  MaybeSetPrepopulateText();
114
115  if (find_tab_helper && find_tab_helper->find_ui_active()) {
116    // A tab with a visible find bar just got selected and we need to show the
117    // find bar but without animation since it was already animated into its
118    // visible state. We also want to reset the window location so that
119    // we don't surprise the user by popping up to the left for no apparent
120    // reason.
121    find_bar_->Show(false);
122  }
123
124  UpdateFindBarForCurrentResult();
125  find_bar_->UpdateFindBarForChangedWebContents();
126}
127
128////////////////////////////////////////////////////////////////////////////////
129// FindBarHost, content::NotificationObserver implementation:
130
131void FindBarController::Observe(int type,
132                                const content::NotificationSource& source,
133                                const content::NotificationDetails& details) {
134  FindTabHelper* find_tab_helper =
135      FindTabHelper::FromWebContents(web_contents_);
136  if (type == chrome::NOTIFICATION_FIND_RESULT_AVAILABLE) {
137    // Don't update for notifications from WebContentses other than the one we
138    // are actively tracking.
139    if (content::Source<WebContents>(source).ptr() == web_contents_) {
140      UpdateFindBarForCurrentResult();
141      if (find_tab_helper->find_result().final_update() &&
142          find_tab_helper->find_result().number_of_matches() == 0) {
143        const base::string16& last_search =
144            find_tab_helper->previous_find_text();
145        const base::string16& current_search = find_tab_helper->find_text();
146        if (last_search.find(current_search) != 0)
147          find_bar_->AudibleAlert();
148      }
149    }
150  } else if (type == content::NOTIFICATION_NAV_ENTRY_COMMITTED) {
151    NavigationController* source_controller =
152        content::Source<NavigationController>(source).ptr();
153    if (source_controller == &web_contents_->GetController()) {
154      content::LoadCommittedDetails* commit_details =
155          content::Details<content::LoadCommittedDetails>(details).ptr();
156      content::PageTransition transition_type =
157          commit_details->entry->GetTransitionType();
158      // We hide the FindInPage window when the user navigates away, except on
159      // reload (and when clicking on anchors within web pages).
160      if (find_bar_->IsFindBarVisible()) {
161        if (content::PageTransitionStripQualifier(transition_type) !=
162            content::PAGE_TRANSITION_RELOAD) {
163          // This is a new navigation (not reload), but we still don't want the
164          // Find box to disappear if the navigation is just to a fragment
165          // within the page.
166          if (commit_details->is_navigation_to_different_page())
167            EndFindSession(kKeepSelectionOnPage, kClearResultsInFindBox);
168        } else {
169          // On Reload we want to make sure FindNext is converted to a full Find
170          // to make sure highlights for inactive matches are repainted.
171          find_tab_helper->set_find_op_aborted(true);
172        }
173      }
174    }
175  }
176}
177
178// static
179gfx::Rect FindBarController::GetLocationForFindbarView(
180    gfx::Rect view_location,
181    const gfx::Rect& dialog_bounds,
182    const gfx::Rect& avoid_overlapping_rect) {
183  if (base::i18n::IsRTL()) {
184    int boundary = dialog_bounds.width() - view_location.width();
185    view_location.set_x(std::min(view_location.x(), boundary));
186  } else {
187    view_location.set_x(std::max(view_location.x(), dialog_bounds.x()));
188  }
189
190  gfx::Rect new_pos = view_location;
191
192  // If the selection rectangle intersects the current position on screen then
193  // we try to move our dialog to the left (right for RTL) of the selection
194  // rectangle.
195  if (!avoid_overlapping_rect.IsEmpty() &&
196      avoid_overlapping_rect.Intersects(new_pos)) {
197    if (base::i18n::IsRTL()) {
198      new_pos.set_x(avoid_overlapping_rect.x() +
199                    avoid_overlapping_rect.width() +
200                    (2 * kMinFindWndDistanceFromSelection));
201
202      // If we moved it off-screen to the right, we won't move it at all.
203      if (new_pos.x() + new_pos.width() > dialog_bounds.width())
204        new_pos = view_location;  // Reset.
205    } else {
206      new_pos.set_x(avoid_overlapping_rect.x() - new_pos.width() -
207        kMinFindWndDistanceFromSelection);
208
209      // If we moved it off-screen to the left, we won't move it at all.
210      if (new_pos.x() < 0)
211        new_pos = view_location;  // Reset.
212    }
213  }
214
215  return new_pos;
216}
217
218void FindBarController::UpdateFindBarForCurrentResult() {
219  FindTabHelper* find_tab_helper =
220      FindTabHelper::FromWebContents(web_contents_);
221  const FindNotificationDetails& find_result = find_tab_helper->find_result();
222
223  // Avoid bug 894389: When a new search starts (and finds something) it reports
224  // an interim match count result of 1 before the scoping effort starts. This
225  // is to provide feedback as early as possible that we will find something.
226  // As you add letters to the search term, this creates a flashing effect when
227  // we briefly show "1 of 1" matches because there is a slight delay until
228  // the scoping effort starts updating the match count. We avoid this flash by
229  // ignoring interim results of 1 if we already have a positive number.
230  if (find_result.number_of_matches() > -1) {
231    if (last_reported_matchcount_ > 0 &&
232        find_result.number_of_matches() == 1 &&
233        !find_result.final_update())
234      return;  // Don't let interim result override match count.
235    last_reported_matchcount_ = find_result.number_of_matches();
236  }
237
238  find_bar_->UpdateUIForFindResult(find_result, find_tab_helper->find_text());
239}
240
241void FindBarController::MaybeSetPrepopulateText() {
242  // Having a per-tab find_string is not compatible with a global find
243  // pasteboard, so we always have the same find text in all find bars. This is
244  // done through the find pasteboard mechanism, so don't set the text here.
245  if (find_bar_->HasGlobalFindPasteboard())
246    return;
247
248  // Find out what we should show in the find text box. Usually, this will be
249  // the last search in this tab, but if no search has been issued in this tab
250  // we use the last search string (from any tab).
251  FindTabHelper* find_tab_helper =
252      FindTabHelper::FromWebContents(web_contents_);
253  base::string16 find_string = find_tab_helper->find_text();
254  if (find_string.empty())
255    find_string = find_tab_helper->previous_find_text();
256  if (find_string.empty()) {
257    Profile* profile =
258        Profile::FromBrowserContext(web_contents_->GetBrowserContext());
259    find_string = FindBarStateFactory::GetLastPrepopulateText(profile);
260  }
261
262  // Update the find bar with existing results and search text, regardless of
263  // whether or not the find bar is visible, so that if it's subsequently
264  // shown it is showing the right state for this tab. We update the find text
265  // _first_ since the FindBarView checks its emptiness to see if it should
266  // clear the result count display when there's nothing in the box.
267  find_bar_->SetFindTextAndSelectedRange(find_string,
268                                         find_tab_helper->selected_range());
269}
270