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_tab_helper.h"
6
7#include <vector>
8
9#include "chrome/browser/chrome_notification_types.h"
10#include "chrome/browser/profiles/profile.h"
11#include "chrome/browser/ui/find_bar/find_bar_state.h"
12#include "chrome/browser/ui/find_bar/find_bar_state_factory.h"
13#include "content/public/browser/notification_service.h"
14#include "content/public/browser/render_view_host.h"
15#include "content/public/browser/web_contents.h"
16#include "content/public/common/stop_find_action.h"
17#include "third_party/WebKit/public/web/WebFindOptions.h"
18#include "ui/gfx/rect_f.h"
19
20using blink::WebFindOptions;
21using content::WebContents;
22
23DEFINE_WEB_CONTENTS_USER_DATA_KEY(FindTabHelper);
24
25// static
26int FindTabHelper::find_request_id_counter_ = -1;
27
28FindTabHelper::FindTabHelper(WebContents* web_contents)
29    : content::WebContentsObserver(web_contents),
30      find_ui_active_(false),
31      find_op_aborted_(false),
32      current_find_request_id_(find_request_id_counter_++),
33      last_search_case_sensitive_(false),
34      last_search_result_() {
35}
36
37FindTabHelper::~FindTabHelper() {
38}
39
40void FindTabHelper::StartFinding(base::string16 search_string,
41                                 bool forward_direction,
42                                 bool case_sensitive) {
43  // Remove the carriage return character, which generally isn't in web content.
44  const base::char16 kInvalidChars[] = { '\r', 0 };
45  base::RemoveChars(search_string, kInvalidChars, &search_string);
46
47  // If search_string is empty, it means FindNext was pressed with a keyboard
48  // shortcut so unless we have something to search for we return early.
49  if (search_string.empty() && find_text_.empty()) {
50    Profile* profile =
51        Profile::FromBrowserContext(web_contents()->GetBrowserContext());
52    base::string16 last_search_prepopulate_text =
53        FindBarStateFactory::GetLastPrepopulateText(profile);
54
55    // Try the last thing we searched for on this tab, then the last thing
56    // searched for on any tab.
57    if (!previous_find_text_.empty())
58      search_string = previous_find_text_;
59    else if (!last_search_prepopulate_text.empty())
60      search_string = last_search_prepopulate_text;
61    else
62      return;
63  }
64
65  // Keep track of the previous search.
66  previous_find_text_ = find_text_;
67
68  // This is a FindNext operation if we are searching for the same text again,
69  // or if the passed in search text is empty (FindNext keyboard shortcut). The
70  // exception to this is if the Find was aborted (then we don't want FindNext
71  // because the highlighting has been cleared and we need it to reappear). We
72  // therefore treat FindNext after an aborted Find operation as a full fledged
73  // Find.
74  bool find_next = (find_text_ == search_string || search_string.empty()) &&
75                   (last_search_case_sensitive_ == case_sensitive) &&
76                   !find_op_aborted_;
77  if (!find_next)
78    current_find_request_id_ = find_request_id_counter_++;
79
80  if (!search_string.empty())
81    find_text_ = search_string;
82  last_search_case_sensitive_ = case_sensitive;
83
84  find_op_aborted_ = false;
85
86  // Keep track of what the last search was across the tabs.
87  Profile* profile =
88      Profile::FromBrowserContext(web_contents()->GetBrowserContext());
89  FindBarState* find_bar_state = FindBarStateFactory::GetForProfile(profile);
90  find_bar_state->set_last_prepopulate_text(find_text_);
91
92  WebFindOptions options;
93  options.forward = forward_direction;
94  options.matchCase = case_sensitive;
95  options.findNext = find_next;
96  web_contents()->Find(current_find_request_id_, find_text_, options);
97}
98
99void FindTabHelper::StopFinding(
100    FindBarController::SelectionAction selection_action) {
101  if (selection_action == FindBarController::kClearSelectionOnPage) {
102    // kClearSelection means the find string has been cleared by the user, but
103    // the UI has not been dismissed. In that case we want to clear the
104    // previously remembered search (http://crbug.com/42639).
105    previous_find_text_ = base::string16();
106  } else {
107    find_ui_active_ = false;
108    if (!find_text_.empty())
109      previous_find_text_ = find_text_;
110  }
111  find_text_.clear();
112  find_op_aborted_ = true;
113  last_search_result_ = FindNotificationDetails();
114
115  content::StopFindAction action;
116  switch (selection_action) {
117    case FindBarController::kClearSelectionOnPage:
118      action = content::STOP_FIND_ACTION_CLEAR_SELECTION;
119      break;
120    case FindBarController::kKeepSelectionOnPage:
121      action = content::STOP_FIND_ACTION_KEEP_SELECTION;
122      break;
123    case FindBarController::kActivateSelectionOnPage:
124      action = content::STOP_FIND_ACTION_ACTIVATE_SELECTION;
125      break;
126    default:
127      NOTREACHED();
128      action = content::STOP_FIND_ACTION_KEEP_SELECTION;
129  }
130  web_contents()->StopFinding(action);
131}
132
133#if defined(OS_ANDROID)
134void FindTabHelper::ActivateNearestFindResult(float x, float y) {
135  if (!find_op_aborted_ && !find_text_.empty()) {
136    web_contents()->GetRenderViewHost()->ActivateNearestFindResult(
137        current_find_request_id_, x, y);
138  }
139}
140
141void FindTabHelper::RequestFindMatchRects(int current_version) {
142  if (!find_op_aborted_ && !find_text_.empty())
143    web_contents()->GetRenderViewHost()->RequestFindMatchRects(current_version);
144}
145#endif
146
147void FindTabHelper::HandleFindReply(int request_id,
148                                    int number_of_matches,
149                                    const gfx::Rect& selection_rect,
150                                    int active_match_ordinal,
151                                    bool final_update) {
152  // Ignore responses for requests that have been aborted.
153  // Ignore responses for requests other than the one we have most recently
154  // issued. That way we won't act on stale results when the user has
155  // already typed in another query.
156  if (!find_op_aborted_ && request_id == current_find_request_id_) {
157    if (number_of_matches == -1)
158      number_of_matches = last_search_result_.number_of_matches();
159    if (active_match_ordinal == -1)
160      active_match_ordinal = last_search_result_.active_match_ordinal();
161
162    gfx::Rect selection = selection_rect;
163    if (final_update && active_match_ordinal == 0)
164      selection = gfx::Rect();
165    else if (selection_rect.IsEmpty())
166      selection = last_search_result_.selection_rect();
167
168    // Notify the UI, automation and any other observers that a find result was
169    // found.
170    last_search_result_ = FindNotificationDetails(
171        request_id, number_of_matches, selection, active_match_ordinal,
172        final_update);
173    content::NotificationService::current()->Notify(
174        chrome::NOTIFICATION_FIND_RESULT_AVAILABLE,
175        content::Source<WebContents>(web_contents()),
176        content::Details<FindNotificationDetails>(&last_search_result_));
177  }
178}
179