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 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 37 AutocompletePopupModel::~AutocompletePopupModel() { 38 } 39 40 bool AutocompletePopupModel::IsOpen() const { 41 return view_->IsOpen(); 42 } 43 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 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 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 131 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 143 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 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 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 225 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 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