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
AutocompletePopupModel(AutocompletePopupView * popup_view,AutocompleteEditModel * edit_model,Profile * profile)25 AutocompletePopupModel::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
~AutocompletePopupModel()37 AutocompletePopupModel::~AutocompletePopupModel() {
38 }
39
IsOpen() const40 bool AutocompletePopupModel::IsOpen() const {
41 return view_->IsOpen();
42 }
43
SetHoveredLine(size_t line)44 void 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
SetSelectedLine(size_t line,bool reset_to_default,bool force)62 void 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, ¤t_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
ResetToDefaultMatch()124 void 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
GetKeywordForMatch(const AutocompleteMatch & match,string16 * keyword) const131 bool 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
GetKeywordForText(const string16 & text,string16 * keyword) const143 bool 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
Move(int count)178 void 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
TryDeletingCurrentItem()193 void 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
GetIconIfExtensionMatch(const AutocompleteMatch & match) const225 const 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
OnResultChanged()234 void 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