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#include "chrome/browser/autocomplete/autocomplete_popup_model.h"
6
7#include <algorithm>
8
9#include "unicode/ubidi.h"
10
11#include "base/string_util.h"
12#include "base/utf_string_conversions.h"
13#include "chrome/browser/autocomplete/autocomplete_edit.h"
14#include "chrome/browser/autocomplete/autocomplete_match.h"
15#include "chrome/browser/autocomplete/autocomplete_popup_view.h"
16#include "chrome/browser/extensions/extension_service.h"
17#include "chrome/browser/profiles/profile.h"
18#include "chrome/browser/search_engines/template_url.h"
19#include "chrome/browser/search_engines/template_url_model.h"
20#include "ui/gfx/rect.h"
21
22///////////////////////////////////////////////////////////////////////////////
23// AutocompletePopupModel
24
25AutocompletePopupModel::AutocompletePopupModel(
26    AutocompletePopupView* popup_view,
27    AutocompleteEditModel* edit_model,
28    Profile* profile)
29    : view_(popup_view),
30      edit_model_(edit_model),
31      profile_(profile),
32      hovered_line_(kNoMatch),
33      selected_line_(kNoMatch) {
34  edit_model->set_popup_model(this);
35}
36
37AutocompletePopupModel::~AutocompletePopupModel() {
38}
39
40bool AutocompletePopupModel::IsOpen() const {
41  return view_->IsOpen();
42}
43
44void AutocompletePopupModel::SetHoveredLine(size_t line) {
45  const bool is_disabling = (line == kNoMatch);
46  DCHECK(is_disabling || (line < result().size()));
47
48  if (line == hovered_line_)
49    return;  // Nothing to do
50
51  // Make sure the old hovered line is redrawn.  No need to redraw the selected
52  // line since selection overrides hover so the appearance won't change.
53  if ((hovered_line_ != kNoMatch) && (hovered_line_ != selected_line_))
54    view_->InvalidateLine(hovered_line_);
55
56  // Change the hover to the new line.
57  hovered_line_ = line;
58  if (!is_disabling && (hovered_line_ != selected_line_))
59    view_->InvalidateLine(hovered_line_);
60}
61
62void AutocompletePopupModel::SetSelectedLine(size_t line,
63                                             bool reset_to_default,
64                                             bool force) {
65  const AutocompleteResult& result = this->result();
66  if (result.empty())
67    return;
68
69  // Cancel the query so the matches don't change on the user.
70  autocomplete_controller()->Stop(false);
71
72  line = std::min(line, result.size() - 1);
73  const AutocompleteMatch& match = result.match_at(line);
74  if (reset_to_default) {
75    manually_selected_match_.Clear();
76  } else {
77    // Track the user's selection until they cancel it.
78    manually_selected_match_.destination_url = match.destination_url;
79    manually_selected_match_.provider_affinity = match.provider;
80    manually_selected_match_.is_history_what_you_typed_match =
81        match.is_history_what_you_typed_match;
82  }
83
84  if (line == selected_line_ && !force)
85    return;  // Nothing else to do.
86
87  // We need to update |selected_line_| before calling OnPopupDataChanged(), so
88  // that when the edit notifies its controller that something has changed, the
89  // controller can get the correct updated data.
90  //
91  // NOTE: We should never reach here with no selected line; the same code that
92  // opened the popup and made it possible to get here should have also set a
93  // selected line.
94  CHECK(selected_line_ != kNoMatch);
95  GURL current_destination(result.match_at(selected_line_).destination_url);
96  view_->InvalidateLine(selected_line_);
97  selected_line_ = line;
98  view_->InvalidateLine(selected_line_);
99
100  // Update the edit with the new data for this match.
101  // TODO(pkasting): If |selected_line_| moves to the controller, this can be
102  // eliminated and just become a call to the observer on the edit.
103  string16 keyword;
104  const bool is_keyword_hint = GetKeywordForMatch(match, &keyword);
105  if (reset_to_default) {
106    string16 inline_autocomplete_text;
107    if ((match.inline_autocomplete_offset != string16::npos) &&
108        (match.inline_autocomplete_offset < match.fill_into_edit.length())) {
109      inline_autocomplete_text =
110          match.fill_into_edit.substr(match.inline_autocomplete_offset);
111    }
112    edit_model_->OnPopupDataChanged(inline_autocomplete_text, NULL,
113                                    keyword, is_keyword_hint);
114  } else {
115    edit_model_->OnPopupDataChanged(match.fill_into_edit, &current_destination,
116                                    keyword, is_keyword_hint);
117  }
118
119  // Repaint old and new selected lines immediately, so that the edit doesn't
120  // appear to update [much] faster than the popup.
121  view_->PaintUpdatesNow();
122}
123
124void AutocompletePopupModel::ResetToDefaultMatch() {
125  const AutocompleteResult& result = this->result();
126  CHECK(!result.empty());
127  SetSelectedLine(result.default_match() - result.begin(), true, false);
128  view_->OnDragCanceled();
129}
130
131bool AutocompletePopupModel::GetKeywordForMatch(const AutocompleteMatch& match,
132                                                string16* keyword) const {
133  // If the current match is a keyword, return that as the selected keyword.
134  if (TemplateURL::SupportsReplacement(match.template_url)) {
135    keyword->assign(match.template_url->keyword());
136    return false;
137  }
138
139  // See if the current match's fill_into_edit corresponds to a keyword.
140  return GetKeywordForText(match.fill_into_edit, keyword);
141}
142
143bool AutocompletePopupModel::GetKeywordForText(const string16& text,
144                                               string16* keyword) const {
145  // Creates keyword_hint first in case |keyword| is a pointer to |text|.
146  const string16 keyword_hint(TemplateURLModel::CleanUserInputKeyword(text));
147
148  // Assume we have no keyword until we find otherwise.
149  keyword->clear();
150
151  if (keyword_hint.empty())
152    return false;
153  if (!profile_->GetTemplateURLModel())
154    return false;
155  profile_->GetTemplateURLModel()->Load();
156
157  // Don't provide a hint if this keyword doesn't support replacement.
158  const TemplateURL* const template_url =
159      profile_->GetTemplateURLModel()->GetTemplateURLForKeyword(keyword_hint);
160  if (!TemplateURL::SupportsReplacement(template_url))
161    return false;
162
163  // Don't provide a hint for inactive/disabled extension keywords.
164  if (template_url->IsExtensionKeyword()) {
165    const Extension* extension = profile_->GetExtensionService()->
166        GetExtensionById(template_url->GetExtensionId(), false);
167    if (!extension ||
168        (profile_->IsOffTheRecord() &&
169         !profile_->GetExtensionService()->
170             IsIncognitoEnabled(extension->id())))
171      return false;
172  }
173
174  keyword->assign(keyword_hint);
175  return true;
176}
177
178void AutocompletePopupModel::Move(int count) {
179  const AutocompleteResult& result = this->result();
180  if (result.empty())
181    return;
182
183  // The user is using the keyboard to change the selection, so stop tracking
184  // hover.
185  SetHoveredLine(kNoMatch);
186
187  // Clamp the new line to [0, result_.count() - 1].
188  const size_t new_line = selected_line_ + count;
189  SetSelectedLine(((count < 0) && (new_line >= selected_line_)) ? 0 : new_line,
190                  false, false);
191}
192
193void AutocompletePopupModel::TryDeletingCurrentItem() {
194  // We could use InfoForCurrentSelection() here, but it seems better to try
195  // and shift-delete the actual selection, rather than any "in progress, not
196  // yet visible" one.
197  if (selected_line_ == kNoMatch)
198    return;
199
200  // Cancel the query so the matches don't change on the user.
201  autocomplete_controller()->Stop(false);
202
203  const AutocompleteMatch& match = result().match_at(selected_line_);
204  if (match.deletable) {
205    const size_t selected_line = selected_line_;
206    const bool was_temporary_text = !manually_selected_match_.empty();
207
208    // This will synchronously notify both the edit and us that the results
209    // have changed, causing both to revert to the default match.
210    autocomplete_controller()->DeleteMatch(match);
211    const AutocompleteResult& result = this->result();
212    if (!result.empty() &&
213        (was_temporary_text || selected_line != selected_line_)) {
214      // Move the selection to the next choice after the deleted one.
215      // SetSelectedLine() will clamp to take care of the case where we deleted
216      // the last item.
217      // TODO(pkasting): Eventually the controller should take care of this
218      // before notifying us, reducing flicker.  At that point the check for
219      // deletability can move there too.
220      SetSelectedLine(selected_line, false, true);
221    }
222  }
223}
224
225const SkBitmap* AutocompletePopupModel::GetIconIfExtensionMatch(
226    const AutocompleteMatch& match) const {
227  if (!match.template_url || !match.template_url->IsExtensionKeyword())
228    return NULL;
229
230  return &profile_->GetExtensionService()->GetOmniboxPopupIcon(
231      match.template_url->GetExtensionId());
232}
233
234void AutocompletePopupModel::OnResultChanged() {
235  const AutocompleteResult& result = this->result();
236  selected_line_ = result.default_match() == result.end() ?
237      kNoMatch : static_cast<size_t>(result.default_match() - result.begin());
238  // There had better not be a nonempty result set with no default match.
239  CHECK((selected_line_ != kNoMatch) || result.empty());
240  manually_selected_match_.Clear();
241  // If we're going to trim the window size to no longer include the hovered
242  // line, turn hover off.  Practically, this shouldn't happen, but it
243  // doesn't hurt to be defensive.
244  if ((hovered_line_ != kNoMatch) && (result.size() <= hovered_line_))
245    SetHoveredLine(kNoMatch);
246
247  view_->UpdatePopupAppearance();
248}
249