• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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, &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 
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