autocomplete_popup_model.cc revision c407dc5cd9bdc5668497f21b26b09d988ab439de
1// Copyright (c) 2010 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/autocomplete/autocomplete_popup_model.h"
6
7#include "base/string_util.h"
8#include "chrome/browser/autocomplete/autocomplete_edit.h"
9#include "chrome/browser/autocomplete/autocomplete_popup_view.h"
10#include "chrome/browser/profile.h"
11#include "chrome/browser/extensions/extensions_service.h"
12#include "chrome/browser/search_engines/template_url.h"
13#include "chrome/browser/search_engines/template_url_model.h"
14#include "chrome/common/notification_service.h"
15#include "third_party/icu/public/common/unicode/ubidi.h"
16
17///////////////////////////////////////////////////////////////////////////////
18// AutocompletePopupModel
19
20AutocompletePopupModel::AutocompletePopupModel(
21    AutocompletePopupView* popup_view,
22    AutocompleteEditModel* edit_model,
23    Profile* profile)
24    : view_(popup_view),
25      edit_model_(edit_model),
26      controller_(new AutocompleteController(profile)),
27      profile_(profile),
28      hovered_line_(kNoMatch),
29      selected_line_(kNoMatch) {
30  registrar_.Add(this, NotificationType::AUTOCOMPLETE_CONTROLLER_RESULT_UPDATED,
31                 Source<AutocompleteController>(controller_.get()));
32}
33
34AutocompletePopupModel::~AutocompletePopupModel() {
35}
36
37void AutocompletePopupModel::SetProfile(Profile* profile) {
38  DCHECK(profile);
39  profile_ = profile;
40  controller_->SetProfile(profile);
41}
42
43void AutocompletePopupModel::StartAutocomplete(
44    const std::wstring& text,
45    const std::wstring& desired_tld,
46    bool prevent_inline_autocomplete,
47    bool prefer_keyword) {
48  // The user is interacting with the edit, so stop tracking hover.
49  SetHoveredLine(kNoMatch);
50
51  manually_selected_match_.Clear();
52
53  controller_->Start(text, desired_tld, prevent_inline_autocomplete,
54                     prefer_keyword, false);
55}
56
57void AutocompletePopupModel::StopAutocomplete() {
58  controller_->Stop(true);
59}
60
61bool AutocompletePopupModel::IsOpen() const {
62  return view_->IsOpen();
63}
64
65void AutocompletePopupModel::SetHoveredLine(size_t line) {
66  const bool is_disabling = (line == kNoMatch);
67  DCHECK(is_disabling || (line < controller_->result().size()));
68
69  if (line == hovered_line_)
70    return;  // Nothing to do
71
72  // Make sure the old hovered line is redrawn.  No need to redraw the selected
73  // line since selection overrides hover so the appearance won't change.
74  if ((hovered_line_ != kNoMatch) && (hovered_line_ != selected_line_))
75    view_->InvalidateLine(hovered_line_);
76
77  // Change the hover to the new line.
78  hovered_line_ = line;
79  if (!is_disabling && (hovered_line_ != selected_line_))
80    view_->InvalidateLine(hovered_line_);
81}
82
83void AutocompletePopupModel::SetSelectedLine(size_t line,
84                                             bool reset_to_default) {
85  const AutocompleteResult& result = controller_->result();
86  CHECK(line < result.size());
87  if (result.empty())
88    return;
89
90  // Cancel the query so the matches don't change on the user.
91  controller_->Stop(false);
92
93  const AutocompleteMatch& match = result.match_at(line);
94  if (reset_to_default) {
95    manually_selected_match_.Clear();
96  } else {
97    // Track the user's selection until they cancel it.
98    manually_selected_match_.destination_url = match.destination_url;
99    manually_selected_match_.provider_affinity = match.provider;
100    manually_selected_match_.is_history_what_you_typed_match =
101        match.is_history_what_you_typed_match;
102  }
103
104  if (line == selected_line_)
105    return;  // Nothing else to do.
106
107  // We need to update |selected_line_| before calling OnPopupDataChanged(), so
108  // that when the edit notifies its controller that something has changed, the
109  // controller can get the correct updated data.
110  //
111  // NOTE: We should never reach here with no selected line; the same code that
112  // opened the popup and made it possible to get here should have also set a
113  // selected line.
114  CHECK(selected_line_ != kNoMatch);
115  GURL current_destination(result.match_at(selected_line_).destination_url);
116  view_->InvalidateLine(selected_line_);
117  selected_line_ = line;
118  view_->InvalidateLine(selected_line_);
119
120  // Update the edit with the new data for this match.
121  // TODO(pkasting): If |selected_line_| moves to the controller, this can be
122  // eliminated and just become a call to the observer on the edit.
123  std::wstring keyword;
124  const bool is_keyword_hint = GetKeywordForMatch(match, &keyword);
125  if (reset_to_default) {
126    std::wstring inline_autocomplete_text;
127    if ((match.inline_autocomplete_offset != std::wstring::npos) &&
128        (match.inline_autocomplete_offset < match.fill_into_edit.length())) {
129      inline_autocomplete_text =
130          match.fill_into_edit.substr(match.inline_autocomplete_offset);
131    }
132    edit_model_->OnPopupDataChanged(inline_autocomplete_text, NULL,
133                                    keyword, is_keyword_hint);
134  } else {
135    edit_model_->OnPopupDataChanged(match.fill_into_edit, &current_destination,
136                                    keyword, is_keyword_hint);
137  }
138
139  // Repaint old and new selected lines immediately, so that the edit doesn't
140  // appear to update [much] faster than the popup.
141  view_->PaintUpdatesNow();
142}
143
144void AutocompletePopupModel::ResetToDefaultMatch() {
145  const AutocompleteResult& result = controller_->result();
146  CHECK(!result.empty());
147  SetSelectedLine(result.default_match() - result.begin(), true);
148  view_->OnDragCanceled();
149}
150
151void AutocompletePopupModel::InfoForCurrentSelection(
152    AutocompleteMatch* match,
153    GURL* alternate_nav_url) const {
154  DCHECK(match != NULL);
155  const AutocompleteResult* result;
156  if (!controller_->done()) {
157    result = &controller_->latest_result();
158    // It's technically possible for |result| to be empty if no provider returns
159    // a synchronous result but the query has not completed synchronously;
160    // pratically, however, that should never actually happen.
161    if (result->empty())
162      return;
163    // The user cannot have manually selected a match, or the query would have
164    // stopped.  So the default match must be the desired selection.
165    *match = *result->default_match();
166  } else {
167    CHECK(IsOpen());
168    // The query isn't running, so the standard result set can't possibly be out
169    // of date.
170    //
171    // NOTE: In practice, it should actually be safe to use
172    // controller_->latest_result() here too, since the controller keeps that
173    // up-to-date.  However we generally try to avoid referring to that.
174    result = &controller_->result();
175    // If there are no results, the popup should be closed (so we should have
176    // failed the CHECK above), and URLsForDefaultMatch() should have been
177    // called instead.
178    CHECK(!result->empty());
179    CHECK(selected_line_ < result->size());
180    *match = result->match_at(selected_line_);
181  }
182  if (alternate_nav_url && manually_selected_match_.empty())
183    *alternate_nav_url = result->alternate_nav_url();
184}
185
186bool AutocompletePopupModel::GetKeywordForMatch(const AutocompleteMatch& match,
187                                                std::wstring* keyword) const {
188  // Assume we have no keyword until we find otherwise.
189  keyword->clear();
190
191  // If the current match is a keyword, return that as the selected keyword.
192  if (TemplateURL::SupportsReplacement(match.template_url)) {
193    keyword->assign(match.template_url->keyword());
194    return false;
195  }
196
197  // See if the current match's fill_into_edit corresponds to a keyword.
198  if (!profile_->GetTemplateURLModel())
199    return false;
200  profile_->GetTemplateURLModel()->Load();
201  const std::wstring keyword_hint(
202      TemplateURLModel::CleanUserInputKeyword(match.fill_into_edit));
203  if (keyword_hint.empty())
204    return false;
205
206  // Don't provide a hint if this keyword doesn't support replacement.
207  const TemplateURL* const template_url =
208      profile_->GetTemplateURLModel()->GetTemplateURLForKeyword(keyword_hint);
209  if (!TemplateURL::SupportsReplacement(template_url))
210    return false;
211
212  keyword->assign(keyword_hint);
213  return true;
214}
215
216AutocompleteLog* AutocompletePopupModel::GetAutocompleteLog() {
217  return new AutocompleteLog(controller_->input().text(),
218      controller_->input().type(), selected_line_, 0, controller_->result());
219}
220
221void AutocompletePopupModel::Move(int count) {
222  const AutocompleteResult& result = controller_->result();
223  if (result.empty())
224    return;
225
226  // The user is using the keyboard to change the selection, so stop tracking
227  // hover.
228  SetHoveredLine(kNoMatch);
229
230  // Clamp the new line to [0, result_.count() - 1].
231  const size_t new_line = selected_line_ + count;
232  SetSelectedLine((((count < 0) && (new_line >= selected_line_)) ?
233      0 : std::min(new_line, result.size() - 1)), false);
234}
235
236void AutocompletePopupModel::TryDeletingCurrentItem() {
237  // We could use InfoForCurrentSelection() here, but it seems better to try
238  // and shift-delete the actual selection, rather than any "in progress, not
239  // yet visible" one.
240  if (selected_line_ == kNoMatch)
241    return;
242
243  // Cancel the query so the matches don't change on the user.
244  controller_->Stop(false);
245
246  const AutocompleteMatch& match =
247      controller_->result().match_at(selected_line_);
248  if (match.deletable) {
249    const size_t selected_line = selected_line_;
250    controller_->DeleteMatch(match);  // This may synchronously notify us that
251                                      // the results have changed.
252    const AutocompleteResult& result = controller_->result();
253    if (!result.empty()) {
254      // Move the selection to the next choice after the deleted one.
255      // TODO(pkasting): Eventually the controller should take care of this
256      // before notifying us, reducing flicker.  At that point the check for
257      // deletability can move there too.
258      SetSelectedLine(std::min(result.size() - 1, selected_line), false);
259    }
260  }
261}
262
263void AutocompletePopupModel::Observe(NotificationType type,
264                                     const NotificationSource& source,
265                                     const NotificationDetails& details) {
266  DCHECK_EQ(NotificationType::AUTOCOMPLETE_CONTROLLER_RESULT_UPDATED,
267            type.value);
268
269  const AutocompleteResult* result =
270      Details<const AutocompleteResult>(details).ptr();
271  selected_line_ = (result->default_match() == result->end()) ?
272      kNoMatch : (result->default_match() - result->begin());
273  // There had better not be a nonempty result set with no default match.
274  CHECK((selected_line_ != kNoMatch) || result->empty());
275  // If we're going to trim the window size to no longer include the hovered
276  // line, turn hover off.  Practically, this shouldn't happen, but it
277  // doesn't hurt to be defensive.
278  if ((hovered_line_ != kNoMatch) && (result->size() <= hovered_line_))
279    SetHoveredLine(kNoMatch);
280
281  view_->UpdatePopupAppearance();
282}
283
284const SkBitmap* AutocompletePopupModel::GetSpecialIconForMatch(
285    const AutocompleteMatch& match) const {
286  if (!match.template_url || !match.template_url->IsExtensionKeyword())
287    return NULL;
288
289  return &profile_->GetExtensionsService()->GetOmniboxIcon(
290      match.template_url->GetExtensionId());
291}
292